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 {
EndpointHandler,
ManifestData,
@ -8,7 +7,7 @@ import type {
SSRManifest,
} from '../../@types/astro';
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 { consoleLogDestination } from '../logger/console.js';
import { error, type LogOptions } from '../logger/core.js';
@ -44,7 +43,6 @@ export class App {
#manifest: SSRManifest;
#manifestData: ManifestData;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#encoder = new TextEncoder();
#logging: LogOptions = {
dest: consoleLogDestination,
level: 'info',
@ -299,36 +297,16 @@ export class App {
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 (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
if (response.headers.get('X-Astro-Response') === 'Not-Found') {
const fourOhFourRequest = new Request(new URL('/404', request.url));
const fourOhFourRouteData = this.match(fourOhFourRequest);
if (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;
}
}
}

View file

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

View file

@ -9,6 +9,7 @@ import type {
} from '../../@types/astro';
import type { Environment, RenderContext } from '../render/index';
import mime from 'mime';
import { isServerLikeOutput } from '../../prerender/utils.js';
import { renderEndpoint } from '../../runtime/server/index.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 { warn } from '../logger/core.js';
import { callMiddleware } from '../middleware/callMiddleware.js';
const encoder = new TextEncoder();
const clientAddressSymbol = Symbol.for('astro.clientAddress');
const clientLocalsSymbol = Symbol.for('astro.locals');
type EndpointCallResult =
| {
type: 'simple';
type SimpleEndpointObject = {
body: string;
encoding?: BufferEncoding;
cookies: AstroCookies;
}
| {
type: 'response';
response: Response;
};
};
type CreateAPIContext = {
request: Request;
@ -100,12 +97,29 @@ export function createAPIContext({
return context;
}
// Return response only
export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>(
mod: EndpointHandler,
env: Environment,
ctx: RenderContext,
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({
request: ctx.request,
params: ctx.params,
@ -128,14 +142,9 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
response = await renderEndpoint(mod, context, env.ssr);
}
if (response instanceof Response) {
attachToResponse(response, context.cookies);
return {
type: 'response',
response,
};
}
// If return simple endpoint object, convert to response
if (!(response instanceof Response)) {
// Validate properties not available in SSR
if (env.ssr && !ctx.route?.prerender) {
if (response.hasOwnProperty('headers')) {
warn(
@ -154,12 +163,50 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
}
}
// Passed during SSG where we don't need to return a full response, as we only need
// to write the `body` to a file directly.
if (returnObjectFormIfAvailable) {
return {
type: 'simple',
body: response.body,
encoding: response.encoding,
cookies: context.cookies,
};
}
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,
});
}
attachToResponse(response, context.cookies);
return response;
}
function isRedirect(statusCode: number) {

View file

@ -1,7 +1,5 @@
import type http from 'http';
import mime from 'mime';
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 { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
@ -176,9 +174,8 @@ export async function handleRoute(
}
// Route successfully matched! Render it.
if (route.type === 'endpoint') {
const result = await callEndpoint(options);
if (result.type === 'response') {
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
const response = await callEndpoint(options);
if (response.headers.get('X-Astro-Response') === 'Not-Found') {
const fourOhFourRoute = await matchRoute('/404', env, manifest);
return handleRoute(
fourOhFourRoute,
@ -192,27 +189,8 @@ export async function handleRoute(
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);
throwIfRedirectNotAllowed(response, config);
await writeWebResponse(res, response);
}
} else {
const result = await renderPage(options);
throwIfRedirectNotAllowed(result, config);