Skip to content

Commit 075eee0

Browse files
ematipicobluwy
andauthored
fix: middleware for API endpoints (#7106)
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
1 parent 826e028 commit 075eee0

File tree

10 files changed

+77
-24
lines changed

10 files changed

+77
-24
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fix middleware for API endpoints that use `Response`, and log a warning for endpoints that don't use `Response`.

packages/astro/src/core/app/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { RouteInfo, SSRManifest as Manifest } from './types';
1010

1111
import mime from 'mime';
1212
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
13-
import { call as callEndpoint, createAPIContext } from '../endpoint/index.js';
13+
import { callEndpoint, createAPIContext } from '../endpoint/index.js';
1414
import { consoleLogDestination } from '../logger/console.js';
1515
import { error, type LogOptions } from '../logger/core.js';
1616
import { callMiddleware } from '../middleware/callMiddleware.js';
@@ -224,6 +224,7 @@ export class App {
224224
let response;
225225
if (onRequest) {
226226
response = await callMiddleware<Response>(
227+
this.#env.logging,
227228
onRequest as MiddlewareResponseHandler,
228229
apiContext,
229230
() => {

packages/astro/src/core/build/generate.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@ import {
2828
} from '../../core/path.js';
2929
import { runHookBuildGenerated } from '../../integrations/index.js';
3030
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
31-
import {
32-
call as callEndpoint,
33-
createAPIContext,
34-
throwIfRedirectNotAllowed,
35-
} from '../endpoint/index.js';
31+
import { callEndpoint, createAPIContext, throwIfRedirectNotAllowed } from '../endpoint/index.js';
3632
import { AstroError } from '../errors/index.js';
3733
import { debug, info } from '../logger/core.js';
3834
import { callMiddleware } from '../middleware/callMiddleware.js';
@@ -495,6 +491,7 @@ async function generatePath(
495491
const onRequest = middleware?.onRequest;
496492
if (onRequest) {
497493
response = await callMiddleware<Response>(
494+
env.logging,
498495
onRequest as MiddlewareResponseHandler,
499496
apiContext,
500497
() => {

packages/astro/src/core/endpoint/dev/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { EndpointHandler } from '../../../@types/astro';
22
import type { LogOptions } from '../../logger/core';
33
import type { SSROptions } from '../../render/dev';
44
import { createRenderContext } from '../../render/index.js';
5-
import { call as callEndpoint } from '../index.js';
5+
import { callEndpoint } from '../index.js';
66

77
export async function call(options: SSROptions, logging: LogOptions) {
88
const {

packages/astro/src/core/endpoint/index.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function createAPIContext({
9393
return context;
9494
}
9595

96-
export async function call<MiddlewareResult = Response | EndpointOutput>(
96+
export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>(
9797
mod: EndpointHandler,
9898
env: Environment,
9999
ctx: RenderContext,
@@ -108,26 +108,25 @@ export async function call<MiddlewareResult = Response | EndpointOutput>(
108108
adapterName: env.adapterName,
109109
});
110110

111-
let response = await renderEndpoint(mod, context, env.ssr);
111+
let response;
112112
if (middleware && middleware.onRequest) {
113-
if (response.body === null) {
114-
const onRequest = middleware.onRequest as MiddlewareEndpointHandler;
115-
response = await callMiddleware<Response | EndpointOutput>(onRequest, context, async () => {
113+
const onRequest = middleware.onRequest as MiddlewareEndpointHandler;
114+
response = await callMiddleware<Response | EndpointOutput>(
115+
env.logging,
116+
onRequest,
117+
context,
118+
async () => {
116119
if (env.mode === 'development' && !isValueSerializable(context.locals)) {
117120
throw new AstroError({
118121
...AstroErrorData.LocalsNotSerializable,
119122
message: AstroErrorData.LocalsNotSerializable.message(ctx.pathname),
120123
});
121124
}
122-
return response;
123-
});
124-
} else {
125-
warn(
126-
env.logging,
127-
'middleware',
128-
"Middleware doesn't work for endpoints that return a simple body. The middleware will be disabled for this page."
129-
);
130-
}
125+
return await renderEndpoint(mod, context, env.ssr);
126+
}
127+
);
128+
} else {
129+
response = await renderEndpoint(mod, context, env.ssr);
131130
}
132131

133132
if (response instanceof Response) {

packages/astro/src/core/middleware/callMiddleware.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro';
22
import { AstroError, AstroErrorData } from '../errors/index.js';
3+
import type { EndpointOutput } from '../../@types/astro';
4+
import { warn } from '../logger/core.js';
5+
import type { Environment } from '../render';
6+
import { bold } from 'kleur/colors';
37

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

5863
return await Promise.resolve(middlewarePromise).then(async (value) => {
64+
if (isEndpointOutput(value)) {
65+
warn(
66+
logging,
67+
'middleware',
68+
'Using simple endpoints can cause unexpected issues in the chain of middleware functions.' +
69+
`\nIt's strongly suggested to use full ${bold('Response')} objects.`
70+
);
71+
}
72+
5973
// first we check if `next` was called
6074
if (nextCalled) {
6175
/**
@@ -99,6 +113,10 @@ export async function callMiddleware<R>(
99113
});
100114
}
101115

102-
function isEndpointResult(response: any): boolean {
103-
return response && typeof response.body !== 'undefined';
116+
function isEndpointOutput(endpointResult: any): endpointResult is EndpointOutput {
117+
return (
118+
!(endpointResult instanceof Response) &&
119+
typeof endpointResult === 'object' &&
120+
typeof endpointResult.body === 'string'
121+
);
104122
}

packages/astro/src/core/render/dev/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export async function renderPage(options: SSROptions): Promise<Response> {
190190
});
191191

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

packages/astro/test/fixtures/middleware-dev/src/middleware.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ const first = defineMiddleware(async (context, next) => {
1111
return new Response(null, {
1212
status: 500,
1313
});
14+
} else if (context.request.url.includes('/api/endpoint')) {
15+
const response = await next();
16+
const object = await response.json();
17+
object.name = 'REDACTED';
18+
return new Response(JSON.stringify(object), {
19+
headers: response.headers,
20+
});
1421
} else {
1522
context.locals.name = 'bar';
1623
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function get() {
2+
const object = {
3+
name: 'Endpoint!!',
4+
};
5+
return new Response(JSON.stringify(object), {
6+
headers: {
7+
'Content-Type': 'application/json',
8+
},
9+
});
10+
}

packages/astro/test/middleware.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,22 @@ describe('Middleware API in PROD mode, SSR', () => {
197197
const $ = cheerio.load(html);
198198
expect($('title').html()).to.not.equal('MiddlewareNoDataReturned');
199199
});
200+
201+
it('should correctly work for API endpoints that return a Response object', async () => {
202+
const app = await fixture.loadTestAdapterApp();
203+
const request = new Request('http://example.com/api/endpoint');
204+
const response = await app.render(request);
205+
expect(response.status).to.equal(200);
206+
expect(response.headers.get('Content-Type')).to.equal('application/json');
207+
});
208+
209+
it('should correctly manipulate the response coming from API endpoints (not simple)', async () => {
210+
const app = await fixture.loadTestAdapterApp();
211+
const request = new Request('http://example.com/api/endpoint');
212+
const response = await app.render(request);
213+
const text = await response.text();
214+
expect(text.includes('REDACTED')).to.be.true;
215+
});
200216
});
201217

202218
describe('Middleware with tailwind', () => {

0 commit comments

Comments
 (0)