Compare commits

...

3 commits

Author SHA1 Message Date
bluwy
5debbc74a1 Fix test 2023-07-07 21:39:44 +08:00
bluwy
e14e9dd1d9 Merge branch 'main' into refactor-endpoint-response-handling 2023-07-07 18:42:54 +08:00
bluwy
d60c16a839 Refactor endpoint response object handling 2023-07-06 23:26:31 +08:00
4 changed files with 115 additions and 118 deletions

View file

@ -1,4 +1,3 @@
import mime from 'mime';
import type { import type {
EndpointHandler, EndpointHandler,
ManifestData, ManifestData,
@ -8,7 +7,7 @@ import type {
SSRManifest, SSRManifest,
} from '../../@types/astro'; } from '../../@types/astro';
import type { SinglePageBuiltModule } from '../build/types'; import type { SinglePageBuiltModule } from '../build/types';
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js'; import { getSetCookiesFromResponse } from '../cookies/index.js';
import { callEndpoint, createAPIContext } from '../endpoint/index.js'; import { callEndpoint, createAPIContext } from '../endpoint/index.js';
import { consoleLogDestination } from '../logger/console.js'; import { consoleLogDestination } from '../logger/console.js';
import { error, type LogOptions } from '../logger/core.js'; import { error, type LogOptions } from '../logger/core.js';
@ -44,7 +43,6 @@ export class App {
#manifest: SSRManifest; #manifest: SSRManifest;
#manifestData: ManifestData; #manifestData: ManifestData;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>; #routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#encoder = new TextEncoder();
#logging: LogOptions = { #logging: LogOptions = {
dest: consoleLogDestination, dest: consoleLogDestination,
level: 'info', level: 'info',
@ -299,36 +297,16 @@ export class App {
mod: handler as any, mod: handler as any,
}); });
const result = await callEndpoint(handler, this.#env, ctx, page.onRequest); const response = await callEndpoint(handler, this.#env, ctx, page.onRequest);
if (result.type === 'response') { if (response.headers.get('X-Astro-Response') === 'Not-Found') {
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') { const fourOhFourRequest = new Request(new URL('/404', request.url));
const fourOhFourRequest = new Request(new URL('/404', request.url)); const fourOhFourRouteData = this.match(fourOhFourRequest);
const fourOhFourRouteData = this.match(fourOhFourRequest); if (fourOhFourRouteData) {
if (fourOhFourRouteData) { return this.render(fourOhFourRequest, fourOhFourRouteData);
return this.render(fourOhFourRequest, fourOhFourRouteData);
}
} }
return result.response;
} else {
const body = result.body;
const headers = new Headers();
const mimeType = mime.getType(url.pathname);
if (mimeType) {
headers.set('Content-Type', `${mimeType};charset=utf-8`);
} else {
headers.set('Content-Type', 'text/plain;charset=utf-8');
}
const bytes = this.#encoder.encode(body);
headers.set('Content-Length', bytes.byteLength.toString());
const response = new Response(bytes, {
status: 200,
headers,
});
attachToResponse(response, result.cookies);
return response;
} }
return response;
} }
} }

View file

@ -8,7 +8,6 @@ import type {
AstroSettings, AstroSettings,
ComponentInstance, ComponentInstance,
EndpointHandler, EndpointHandler,
EndpointOutput,
GetStaticPathsItem, GetStaticPathsItem,
ImageTransform, ImageTransform,
MiddlewareHandler, MiddlewareHandler,
@ -556,18 +555,13 @@ async function generatePath(
if (pageData.route.type === 'endpoint') { if (pageData.route.type === 'endpoint') {
const endpointHandler = mod as unknown as EndpointHandler; const endpointHandler = mod as unknown as EndpointHandler;
const result = await callEndpoint( const result = await callEndpoint(endpointHandler, env, renderContext, onRequest, true);
endpointHandler,
env,
renderContext,
onRequest as MiddlewareHandler<Response | EndpointOutput>
);
if (result.type === 'response') { if (result instanceof Response) {
throwIfRedirectNotAllowed(result.response, opts.settings.config); throwIfRedirectNotAllowed(result, opts.settings.config);
// If there's no body, do nothing // If there's no body, do nothing
if (!result.response.body) return; if (!result.body) return;
const ab = await result.response.arrayBuffer(); const ab = await result.arrayBuffer();
body = new Uint8Array(ab); body = new Uint8Array(ab);
} else { } else {
body = result.body; body = result.body;

View file

@ -9,6 +9,7 @@ import type {
} from '../../@types/astro'; } from '../../@types/astro';
import type { Environment, RenderContext } from '../render/index'; import type { Environment, RenderContext } from '../render/index';
import mime from 'mime';
import { isServerLikeOutput } from '../../prerender/utils.js'; import { isServerLikeOutput } from '../../prerender/utils.js';
import { renderEndpoint } from '../../runtime/server/index.js'; import { renderEndpoint } from '../../runtime/server/index.js';
import { ASTRO_VERSION } from '../constants.js'; import { ASTRO_VERSION } from '../constants.js';
@ -16,20 +17,16 @@ import { AstroCookies, attachToResponse } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js';
import { warn } from '../logger/core.js'; import { warn } from '../logger/core.js';
import { callMiddleware } from '../middleware/callMiddleware.js'; import { callMiddleware } from '../middleware/callMiddleware.js';
const encoder = new TextEncoder();
const clientAddressSymbol = Symbol.for('astro.clientAddress'); const clientAddressSymbol = Symbol.for('astro.clientAddress');
const clientLocalsSymbol = Symbol.for('astro.locals'); const clientLocalsSymbol = Symbol.for('astro.locals');
type EndpointCallResult = type SimpleEndpointObject = {
| { body: string;
type: 'simple'; encoding?: BufferEncoding;
body: string; };
encoding?: BufferEncoding;
cookies: AstroCookies;
}
| {
type: 'response';
response: Response;
};
type CreateAPIContext = { type CreateAPIContext = {
request: Request; request: Request;
@ -100,12 +97,29 @@ export function createAPIContext({
return context; return context;
} }
// Return response only
export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>( export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>(
mod: EndpointHandler, mod: EndpointHandler,
env: Environment, env: Environment,
ctx: RenderContext, ctx: RenderContext,
onRequest?: MiddlewareHandler<MiddlewareResult> | undefined onRequest?: MiddlewareHandler<MiddlewareResult> | undefined
): Promise<EndpointCallResult> { ): Promise<Response>;
// Return response or a simple endpoint object (used for SSG)
export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>(
mod: EndpointHandler,
env: Environment,
ctx: RenderContext,
onRequest?: MiddlewareHandler<MiddlewareResult> | undefined,
returnObjectFormIfAvailable?: boolean
): Promise<Response | SimpleEndpointObject>;
// Base implementation
export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>(
mod: EndpointHandler,
env: Environment,
ctx: RenderContext,
onRequest?: MiddlewareHandler<MiddlewareResult> | undefined,
returnObjectFormIfAvailable?: boolean
): Promise<Response | SimpleEndpointObject> {
const context = createAPIContext({ const context = createAPIContext({
request: ctx.request, request: ctx.request,
params: ctx.params, params: ctx.params,
@ -128,38 +142,71 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
response = await renderEndpoint(mod, context, env.ssr); response = await renderEndpoint(mod, context, env.ssr);
} }
if (response instanceof Response) { // If return simple endpoint object, convert to response
attachToResponse(response, context.cookies); if (!(response instanceof Response)) {
return { // Validate properties not available in SSR
type: 'response', if (env.ssr && !ctx.route?.prerender) {
response, if (response.hasOwnProperty('headers')) {
}; warn(
} env.logging,
'ssr',
'Setting headers is not supported when returning an object. Please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.'
);
}
if (env.ssr && !ctx.route?.prerender) { if (response.encoding) {
if (response.hasOwnProperty('headers')) { warn(
warn( env.logging,
env.logging, 'ssr',
'ssr', '`encoding` is ignored in SSR. To return a charset other than UTF-8, please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.'
'Setting headers is not supported when returning an object. Please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.' );
); }
} }
if (response.encoding) { // Passed during SSG where we don't need to return a full response, as we only need
warn( // to write the `body` to a file directly.
env.logging, if (returnObjectFormIfAvailable) {
'ssr', return {
'`encoding` is ignored in SSR. To return a charset other than UTF-8, please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.' body: response.body,
); encoding: response.encoding,
};
} }
let body: BodyInit;
const headers = new Headers();
const pathname = ctx.route
? // Try the static route `pathname`
ctx.route.pathname ??
// Dynamic routes don't include `pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg')
ctx.route.segments.map((s) => s.map((p) => p.content).join('')).join('/')
: // Fallback to pathname of the request
ctx.pathname;
const mimeType = mime.getType(pathname) || 'text/plain';
headers.set('Content-Type', `${mimeType};charset=utf-8`);
if (typeof Buffer !== 'undefined' && Buffer.from) {
body = Buffer.from(response.body, response.encoding);
} else if (
response.encoding == null ||
response.encoding === 'utf8' ||
response.encoding === 'utf-8'
) {
body = encoder.encode(response.body);
headers.set('Content-Length', body.byteLength.toString());
} else {
body = response.body;
}
response = new Response(body, {
status: 200,
headers,
});
} }
return { attachToResponse(response, context.cookies);
type: 'simple',
body: response.body, return response;
encoding: response.encoding,
cookies: context.cookies,
};
} }
function isRedirect(statusCode: number) { function isRedirect(statusCode: number) {

View file

@ -1,7 +1,5 @@
import type http from 'http'; import type http from 'http';
import mime from 'mime';
import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro'; import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro';
import { attachToResponse } from '../core/cookies/index.js';
import { call as callEndpoint } from '../core/endpoint/dev/index.js'; import { call as callEndpoint } from '../core/endpoint/dev/index.js';
import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js'; import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
import { AstroErrorData, isAstroError } from '../core/errors/index.js'; import { AstroErrorData, isAstroError } from '../core/errors/index.js';
@ -176,43 +174,23 @@ export async function handleRoute(
} }
// Route successfully matched! Render it. // Route successfully matched! Render it.
if (route.type === 'endpoint') { if (route.type === 'endpoint') {
const result = await callEndpoint(options); const response = await callEndpoint(options);
if (result.type === 'response') { if (response.headers.get('X-Astro-Response') === 'Not-Found') {
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') { const fourOhFourRoute = await matchRoute('/404', env, manifest);
const fourOhFourRoute = await matchRoute('/404', env, manifest); return handleRoute(
return handleRoute( fourOhFourRoute,
fourOhFourRoute, new URL('/404', url),
new URL('/404', url), '/404',
'/404', body,
body, origin,
origin, env,
env, manifest,
manifest, req,
req, res
res );
);
}
throwIfRedirectNotAllowed(result.response, config);
await writeWebResponse(res, result.response);
} else {
let contentType = 'text/plain';
// Dynamic routes don't include `route.pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg')
const filepath =
route.pathname ||
route.segments.map((segment) => segment.map((p) => p.content).join('')).join('/');
const computedMimeType = mime.getType(filepath);
if (computedMimeType) {
contentType = computedMimeType;
}
const response = new Response(Buffer.from(result.body, result.encoding), {
status: 200,
headers: {
'Content-Type': `${contentType};charset=utf-8`,
},
});
attachToResponse(response, result.cookies);
await writeWebResponse(res, response);
} }
throwIfRedirectNotAllowed(response, config);
await writeWebResponse(res, response);
} else { } else {
const result = await renderPage(options); const result = await renderPage(options);
throwIfRedirectNotAllowed(result, config); throwIfRedirectNotAllowed(result, config);