Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: middleware for API endpoints #7106

Merged
merged 2 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/long-starfishes-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fix middleware for API endpoints that use `Response`, and log a warning for endpoints that don't use `Response`.
3 changes: 2 additions & 1 deletion packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { RouteInfo, SSRManifest as Manifest } from './types';

import mime from 'mime';
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
import { call as callEndpoint, createAPIContext } from '../endpoint/index.js';
import { callEndpoint, createAPIContext } from '../endpoint/index.js';
import { consoleLogDestination } from '../logger/console.js';
import { error, type LogOptions } from '../logger/core.js';
import { callMiddleware } from '../middleware/callMiddleware.js';
Expand Down Expand Up @@ -224,6 +224,7 @@ export class App {
let response;
if (onRequest) {
response = await callMiddleware<Response>(
this.#env.logging,
onRequest as MiddlewareResponseHandler,
apiContext,
() => {
Expand Down
7 changes: 2 additions & 5 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ import {
} from '../../core/path.js';
import { runHookBuildGenerated } from '../../integrations/index.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import {
call as callEndpoint,
createAPIContext,
throwIfRedirectNotAllowed,
} from '../endpoint/index.js';
import { callEndpoint, createAPIContext, throwIfRedirectNotAllowed } from '../endpoint/index.js';
import { AstroError } from '../errors/index.js';
import { debug, info } from '../logger/core.js';
import { callMiddleware } from '../middleware/callMiddleware.js';
Expand Down Expand Up @@ -495,6 +491,7 @@ async function generatePath(
const onRequest = middleware?.onRequest;
if (onRequest) {
response = await callMiddleware<Response>(
env.logging,
onRequest as MiddlewareResponseHandler,
apiContext,
() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/endpoint/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { EndpointHandler } from '../../../@types/astro';
import type { LogOptions } from '../../logger/core';
import type { SSROptions } from '../../render/dev';
import { createRenderContext } from '../../render/index.js';
import { call as callEndpoint } from '../index.js';
import { callEndpoint } from '../index.js';

export async function call(options: SSROptions, logging: LogOptions) {
const {
Expand Down
27 changes: 13 additions & 14 deletions packages/astro/src/core/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function createAPIContext({
return context;
}

export async function call<MiddlewareResult = Response | EndpointOutput>(
export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>(
mod: EndpointHandler,
env: Environment,
ctx: RenderContext,
Expand All @@ -108,26 +108,25 @@ export async function call<MiddlewareResult = Response | EndpointOutput>(
adapterName: env.adapterName,
});

let response = await renderEndpoint(mod, context, env.ssr);
let response;
if (middleware && middleware.onRequest) {
if (response.body === null) {
const onRequest = middleware.onRequest as MiddlewareEndpointHandler;
response = await callMiddleware<Response | EndpointOutput>(onRequest, context, async () => {
const onRequest = middleware.onRequest as MiddlewareEndpointHandler;
response = await callMiddleware<Response | EndpointOutput>(
env.logging,
onRequest,
context,
async () => {
if (env.mode === 'development' && !isValueSerializable(context.locals)) {
throw new AstroError({
...AstroErrorData.LocalsNotSerializable,
message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname),
});
}
return response;
});
} else {
warn(
env.logging,
'middleware',
"Middleware doesn't work for endpoints that return a simple body. The middleware will be disabled for this page."
);
}
return await renderEndpoint(mod, context, env.ssr);
}
);
} else {
response = await renderEndpoint(mod, context, env.ssr);
}

if (response instanceof Response) {
Expand Down
22 changes: 20 additions & 2 deletions packages/astro/src/core/middleware/callMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro';
import { AstroError, AstroErrorData } from '../errors/index.js';
import type { EndpointOutput } from '../../@types/astro';
import { warn } from '../logger/core.js';
import type { Environment } from '../render';
import { bold } from 'kleur/colors';

/**
* Utility function that is in charge of calling the middleware.
Expand Down Expand Up @@ -36,6 +40,7 @@ import { AstroError, AstroErrorData } from '../errors/index.js';
* @param responseFunction A callback function that should return a promise with the response
*/
export async function callMiddleware<R>(
logging: Environment['logging'],
onRequest: MiddlewareHandler<R>,
apiContext: APIContext,
responseFunction: () => Promise<R>
Expand All @@ -56,6 +61,15 @@ export async function callMiddleware<R>(
let middlewarePromise = onRequest(apiContext, next);

return await Promise.resolve(middlewarePromise).then(async (value) => {
if (isEndpointOutput(value)) {
warn(
logging,
'middleware',
'Using simple endpoints can cause unexpected issues in the chain of middleware functions.' +
`\nIt's strongly suggested to use full ${bold('Response')} objects.`
);
}

// first we check if `next` was called
if (nextCalled) {
/**
Expand Down Expand Up @@ -99,6 +113,10 @@ export async function callMiddleware<R>(
});
}

function isEndpointResult(response: any): boolean {
return response && typeof response.body !== 'undefined';
function isEndpointOutput(endpointResult: any): endpointResult is EndpointOutput {
return (
!(endpointResult instanceof Response) &&
typeof endpointResult === 'object' &&
typeof endpointResult.body === 'string'
);
}
2 changes: 1 addition & 1 deletion packages/astro/src/core/render/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export async function renderPage(options: SSROptions): Promise<Response> {
});

const onRequest = options.middleware.onRequest as MiddlewareResponseHandler;
const response = await callMiddleware<Response>(onRequest, apiContext, () => {
const response = await callMiddleware<Response>(env.logging, onRequest, apiContext, () => {
return coreRenderPage({ mod, renderContext, env: options.env, apiContext });
});

Expand Down
7 changes: 7 additions & 0 deletions packages/astro/test/fixtures/middleware-dev/src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const first = defineMiddleware(async (context, next) => {
return new Response(null, {
status: 500,
});
} else if (context.request.url.includes('/api/endpoint')) {
const response = await next();
const object = await response.json();
object.name = 'REDACTED';
return new Response(JSON.stringify(object), {
headers: response.headers,
});
} else {
context.locals.name = 'bar';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function get() {
const object = {
name: 'Endpoint!!',
};
return new Response(JSON.stringify(object), {
headers: {
'Content-Type': 'application/json',
},
});
}
16 changes: 16 additions & 0 deletions packages/astro/test/middleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ describe('Middleware API in PROD mode, SSR', () => {
const $ = cheerio.load(html);
expect($('title').html()).to.not.equal('MiddlewareNoDataReturned');
});

it('should correctly work for API endpoints that return a Response object', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/api/endpoint');
const response = await app.render(request);
expect(response.status).to.equal(200);
expect(response.headers.get('Content-Type')).to.equal('application/json');
});

it('should correctly manipulate the response coming from API endpoints (not simple)', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/api/endpoint');
const response = await app.render(request);
const text = await response.text();
expect(text.includes('REDACTED')).to.be.true;
});
});

describe('Middleware with tailwind', () => {
Expand Down