Deprecate simple objects from endpoints (#8132)
This commit is contained in:
parent
fd6261dad4
commit
767eb68666
20 changed files with 243 additions and 201 deletions
26
.changeset/yellow-tips-cover.md
Normal file
26
.changeset/yellow-tips-cover.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Deprecate returning simple objects from endpoints. Endpoints should only return a `Response`.
|
||||||
|
|
||||||
|
To return a result with a custom encoding not supported by a `Response`, you can use the `ResponseWithEncoding` utility class instead.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function GET() {
|
||||||
|
return {
|
||||||
|
body: '...',
|
||||||
|
encoding: 'binary',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function GET({ ResponseWithEncoding }) {
|
||||||
|
return new ResponseWithEncoding('...', undefined, 'binary');
|
||||||
|
}
|
||||||
|
```
|
|
@ -23,6 +23,7 @@ import type { LogOptions, LoggerLevel } from '../core/logger/core';
|
||||||
import type { AstroIntegrationLogger } from '../core/logger/core';
|
import type { AstroIntegrationLogger } from '../core/logger/core';
|
||||||
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
|
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
|
||||||
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
||||||
|
import type { ResponseWithEncoding } from '../core/endpoint/index.js';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
MarkdownHeading,
|
MarkdownHeading,
|
||||||
|
@ -1963,12 +1964,13 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
locals: App.Locals;
|
locals: App.Locals;
|
||||||
|
ResponseWithEncoding: typeof ResponseWithEncoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EndpointOutput =
|
export type EndpointOutput =
|
||||||
| {
|
| {
|
||||||
body: Body;
|
body: Body;
|
||||||
encoding?: Exclude<BufferEncoding, 'binary'>;
|
encoding?: BufferEncoding;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
body: Uint8Array;
|
body: Uint8Array;
|
||||||
|
|
|
@ -170,7 +170,7 @@ export class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SSRRoutePipeline.isResponse(response, routeData.type)) {
|
if (routeData.type === 'page' || routeData.type === 'redirect') {
|
||||||
if (STATUS_CODES.has(response.status)) {
|
if (STATUS_CODES.has(response.status)) {
|
||||||
return this.#renderError(request, {
|
return this.#renderError(request, {
|
||||||
response,
|
response,
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import type { Environment } from '../render';
|
import type { Environment } from '../render';
|
||||||
import type { EndpointCallResult } from '../endpoint/index.js';
|
|
||||||
import mime from 'mime';
|
|
||||||
import { attachCookiesToResponse } from '../cookies/index.js';
|
|
||||||
import { Pipeline } from '../pipeline.js';
|
import { Pipeline } from '../pipeline.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,39 +13,16 @@ export class EndpointNotFoundError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SSRRoutePipeline extends Pipeline {
|
export class SSRRoutePipeline extends Pipeline {
|
||||||
#encoder = new TextEncoder();
|
|
||||||
|
|
||||||
constructor(env: Environment) {
|
constructor(env: Environment) {
|
||||||
super(env);
|
super(env);
|
||||||
this.setEndpointHandler(this.#ssrEndpointHandler);
|
this.setEndpointHandler(this.#ssrEndpointHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is responsible for handling the result coming from an endpoint.
|
// This function is responsible for handling the result coming from an endpoint.
|
||||||
async #ssrEndpointHandler(request: Request, response: EndpointCallResult): Promise<Response> {
|
async #ssrEndpointHandler(request: Request, response: Response): Promise<Response> {
|
||||||
if (response.type === 'response') {
|
if (response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||||
if (response.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
throw new EndpointNotFoundError(response);
|
||||||
throw new EndpointNotFoundError(response.response);
|
|
||||||
}
|
|
||||||
return response.response;
|
|
||||||
} else {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
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 =
|
|
||||||
response.encoding !== 'binary' ? this.#encoder.encode(response.body) : response.body;
|
|
||||||
headers.set('Content-Length', bytes.byteLength.toString());
|
|
||||||
|
|
||||||
const newResponse = new Response(bytes, {
|
|
||||||
status: 200,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
attachCookiesToResponse(newResponse, response.cookies);
|
|
||||||
return newResponse;
|
|
||||||
}
|
}
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,11 @@ import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
|
||||||
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
|
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
|
||||||
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
|
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
|
||||||
import type { SSRManifest } from '../app/types';
|
import type { SSRManifest } from '../app/types';
|
||||||
import type { AstroConfig, AstroSettings, RouteType, SSRLoadedRenderer } from '../../@types/astro';
|
import type { AstroConfig, AstroSettings, SSRLoadedRenderer } from '../../@types/astro';
|
||||||
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
|
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
|
||||||
import type { EndpointCallResult } from '../endpoint';
|
|
||||||
import { createEnvironment } from '../render/index.js';
|
import { createEnvironment } from '../render/index.js';
|
||||||
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import { createAssetLink } from '../render/ssr-element.js';
|
import { createAssetLink } from '../render/ssr-element.js';
|
||||||
import type { BufferEncoding } from 'vfile';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files.
|
* This pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files.
|
||||||
|
@ -20,10 +18,6 @@ export class BuildPipeline extends Pipeline {
|
||||||
#internals: BuildInternals;
|
#internals: BuildInternals;
|
||||||
#staticBuildOptions: StaticBuildOptions;
|
#staticBuildOptions: StaticBuildOptions;
|
||||||
#manifest: SSRManifest;
|
#manifest: SSRManifest;
|
||||||
#currentEndpointBody?: {
|
|
||||||
body: string | Uint8Array;
|
|
||||||
encoding: BufferEncoding;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
staticBuildOptions: StaticBuildOptions,
|
staticBuildOptions: StaticBuildOptions,
|
||||||
|
@ -163,49 +157,7 @@ export class BuildPipeline extends Pipeline {
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleEndpointResult(request: Request, response: EndpointCallResult): Promise<Response> {
|
async #handleEndpointResult(_: Request, response: Response): Promise<Response> {
|
||||||
if (response.type === 'response') {
|
return response;
|
||||||
if (!response.response.body) {
|
|
||||||
return new Response(null);
|
|
||||||
}
|
|
||||||
const ab = await response.response.arrayBuffer();
|
|
||||||
const body = new Uint8Array(ab);
|
|
||||||
this.#currentEndpointBody = {
|
|
||||||
body: body,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
};
|
|
||||||
return response.response;
|
|
||||||
} else {
|
|
||||||
if (response.encoding) {
|
|
||||||
this.#currentEndpointBody = {
|
|
||||||
body: response.body,
|
|
||||||
encoding: response.encoding,
|
|
||||||
};
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.set('X-Astro-Encoding', response.encoding);
|
|
||||||
return new Response(response.body, {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return new Response(response.body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async computeBodyAndEncoding(
|
|
||||||
routeType: RouteType,
|
|
||||||
response: Response
|
|
||||||
): Promise<{
|
|
||||||
body: string | Uint8Array;
|
|
||||||
encoding: BufferEncoding;
|
|
||||||
}> {
|
|
||||||
const encoding = response.headers.get('X-Astro-Encoding') ?? 'utf-8';
|
|
||||||
if (this.#currentEndpointBody) {
|
|
||||||
const currentEndpointBody = this.#currentEndpointBody;
|
|
||||||
this.#currentEndpointBody = undefined;
|
|
||||||
return currentEndpointBody;
|
|
||||||
} else {
|
|
||||||
return { body: await response.text(), encoding: encoding as BufferEncoding };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -567,20 +567,16 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
|
||||||
} else {
|
} else {
|
||||||
// If there's no body, do nothing
|
// If there's no body, do nothing
|
||||||
if (!response.body) return;
|
if (!response.body) return;
|
||||||
const result = await pipeline.computeBodyAndEncoding(renderContext.route.type, response);
|
body = Buffer.from(await response.arrayBuffer());
|
||||||
body = result.body;
|
encoding = (response.headers.get('X-Astro-Encoding') as BufferEncoding | null) ?? 'utf-8';
|
||||||
encoding = result.encoding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const outFolder = getOutFolder(pipeline.getConfig(), pathname, pageData.route.type);
|
const outFolder = getOutFolder(pipeline.getConfig(), pathname, pageData.route.type);
|
||||||
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, pageData.route.type);
|
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, pageData.route.type);
|
||||||
pageData.route.distURL = outFile;
|
pageData.route.distURL = outFile;
|
||||||
const possibleEncoding = response.headers.get('X-Astro-Encoding');
|
|
||||||
if (possibleEncoding) {
|
|
||||||
encoding = possibleEncoding as BufferEncoding;
|
|
||||||
}
|
|
||||||
await fs.promises.mkdir(outFolder, { recursive: true });
|
await fs.promises.mkdir(outFolder, { recursive: true });
|
||||||
await fs.promises.writeFile(outFile, body, encoding ?? 'utf-8');
|
await fs.promises.writeFile(outFile, body, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
MiddlewareHandler,
|
MiddlewareHandler,
|
||||||
Params,
|
Params,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
|
import mime from 'mime';
|
||||||
import type { Environment, RenderContext } from '../render/index';
|
import type { Environment, RenderContext } from '../render/index';
|
||||||
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';
|
||||||
|
@ -14,19 +15,11 @@ 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');
|
||||||
|
|
||||||
export type EndpointCallResult =
|
|
||||||
| (EndpointOutput & {
|
|
||||||
type: 'simple';
|
|
||||||
cookies: AstroCookies;
|
|
||||||
})
|
|
||||||
| {
|
|
||||||
type: 'response';
|
|
||||||
response: Response;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CreateAPIContext = {
|
type CreateAPIContext = {
|
||||||
request: Request;
|
request: Request;
|
||||||
params: Params;
|
params: Params;
|
||||||
|
@ -62,6 +55,7 @@ export function createAPIContext({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
ResponseWithEncoding,
|
||||||
url: new URL(request.url),
|
url: new URL(request.url),
|
||||||
get clientAddress() {
|
get clientAddress() {
|
||||||
if (!(clientAddressSymbol in request)) {
|
if (!(clientAddressSymbol in request)) {
|
||||||
|
@ -96,12 +90,37 @@ export function createAPIContext({
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResponseParameters = ConstructorParameters<typeof Response>;
|
||||||
|
|
||||||
|
export class ResponseWithEncoding extends Response {
|
||||||
|
constructor(body: ResponseParameters[0], init: ResponseParameters[1], encoding?: BufferEncoding) {
|
||||||
|
// If a body string is given, try to encode it to preserve the behaviour as simple objects.
|
||||||
|
// We don't do the full handling as simple objects so users can control how headers are set instead.
|
||||||
|
if (typeof body === 'string') {
|
||||||
|
// In NodeJS, we can use Buffer.from which supports all BufferEncoding
|
||||||
|
if (typeof Buffer !== 'undefined' && Buffer.from) {
|
||||||
|
body = Buffer.from(body, encoding);
|
||||||
|
}
|
||||||
|
// In non-NodeJS, use the web-standard TextEncoder for utf-8 strings
|
||||||
|
else if (encoding == null || encoding === 'utf8' || encoding === 'utf-8') {
|
||||||
|
body = encoder.encode(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super(body, init);
|
||||||
|
|
||||||
|
if (encoding) {
|
||||||
|
this.headers.set('X-Astro-Encoding', encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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> {
|
||||||
const context = createAPIContext({
|
const context = createAPIContext({
|
||||||
request: ctx.request,
|
request: ctx.request,
|
||||||
params: ctx.params,
|
params: ctx.params,
|
||||||
|
@ -124,15 +143,30 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
|
||||||
response = await renderEndpoint(mod, context, env.ssr, env.logging);
|
response = await renderEndpoint(mod, context, env.ssr, env.logging);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isEndpointSSR = env.ssr && !ctx.route?.prerender;
|
||||||
|
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
|
if (isEndpointSSR && response.headers.get('X-Astro-Encoding')) {
|
||||||
|
warn(
|
||||||
|
env.logging,
|
||||||
|
'ssr',
|
||||||
|
'`ResponseWithEncoding` is ignored in SSR. Please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.'
|
||||||
|
);
|
||||||
|
}
|
||||||
attachCookiesToResponse(response, context.cookies);
|
attachCookiesToResponse(response, context.cookies);
|
||||||
return {
|
return response;
|
||||||
type: 'response',
|
|
||||||
response,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.ssr && !ctx.route?.prerender) {
|
// The endpoint returned a simple object, convert it to a Response
|
||||||
|
|
||||||
|
// TODO: Remove in Astro 4.0
|
||||||
|
warn(
|
||||||
|
env.logging,
|
||||||
|
'astro',
|
||||||
|
`${ctx.route.component} returns a simple object which is deprecated. Please return an instance of Response. See https://docs.astro.build/en/core-concepts/endpoints/#server-endpoints-api-routes for more information.`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isEndpointSSR) {
|
||||||
if (response.hasOwnProperty('headers')) {
|
if (response.hasOwnProperty('headers')) {
|
||||||
warn(
|
warn(
|
||||||
env.logging,
|
env.logging,
|
||||||
|
@ -150,9 +184,58 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
let body: BodyInit;
|
||||||
...response,
|
const headers = new Headers();
|
||||||
type: 'simple',
|
|
||||||
cookies: context.cookies,
|
// Try to get the MIME type for this route
|
||||||
};
|
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`);
|
||||||
|
|
||||||
|
// Save encoding to X-Astro-Encoding to be used later during SSG with `fs.writeFile`.
|
||||||
|
// It won't work in SSR and is already warned above.
|
||||||
|
if (response.encoding) {
|
||||||
|
headers.set('X-Astro-Encoding', response.encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Uint8Array (binary), it can passed to Response directly
|
||||||
|
if (response.body instanceof Uint8Array) {
|
||||||
|
body = response.body;
|
||||||
|
headers.set('Content-Length', body.byteLength.toString());
|
||||||
|
}
|
||||||
|
// In NodeJS, we can use Buffer.from which supports all BufferEncoding
|
||||||
|
else if (typeof Buffer !== 'undefined' && Buffer.from) {
|
||||||
|
body = Buffer.from(response.body, response.encoding);
|
||||||
|
headers.set('Content-Length', body.byteLength.toString());
|
||||||
|
}
|
||||||
|
// In non-NodeJS, use the web-standard TextEncoder for utf-8 strings only
|
||||||
|
// to calculate the content length
|
||||||
|
else if (
|
||||||
|
response.encoding == null ||
|
||||||
|
response.encoding === 'utf8' ||
|
||||||
|
response.encoding === 'utf-8'
|
||||||
|
) {
|
||||||
|
body = encoder.encode(response.body);
|
||||||
|
headers.set('Content-Length', body.byteLength.toString());
|
||||||
|
}
|
||||||
|
// Fallback pass it to Response directly. It will mainly rely on X-Astro-Encoding
|
||||||
|
// to be further processed in SSG.
|
||||||
|
else {
|
||||||
|
body = response.body;
|
||||||
|
// NOTE: Can't calculate the content length as we can't encode to figure out the real length.
|
||||||
|
// But also because we don't need the length for SSG as it's only being written to disk.
|
||||||
|
}
|
||||||
|
|
||||||
|
response = new Response(body, {
|
||||||
|
status: 200,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
attachCookiesToResponse(response, context.cookies);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { type RenderContext, type Environment } from './render/index.js';
|
import { type RenderContext, type Environment } from './render/index.js';
|
||||||
import { type EndpointCallResult, callEndpoint, createAPIContext } from './endpoint/index.js';
|
import { callEndpoint, createAPIContext } from './endpoint/index.js';
|
||||||
import type {
|
import type {
|
||||||
MiddlewareHandler,
|
MiddlewareHandler,
|
||||||
MiddlewareResponseHandler,
|
MiddlewareResponseHandler,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
MiddlewareEndpointHandler,
|
MiddlewareEndpointHandler,
|
||||||
RouteType,
|
|
||||||
EndpointHandler,
|
EndpointHandler,
|
||||||
} from '../@types/astro';
|
} from '../@types/astro';
|
||||||
import { callMiddleware } from './middleware/callMiddleware.js';
|
import { callMiddleware } from './middleware/callMiddleware.js';
|
||||||
|
@ -13,7 +12,7 @@ import { renderPage } from './render/core.js';
|
||||||
|
|
||||||
type EndpointResultHandler = (
|
type EndpointResultHandler = (
|
||||||
originalRequest: Request,
|
originalRequest: Request,
|
||||||
result: EndpointCallResult
|
result: Response
|
||||||
) => Promise<Response> | Response;
|
) => Promise<Response> | Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,7 +75,7 @@ export class Pipeline {
|
||||||
componentInstance,
|
componentInstance,
|
||||||
this.#onRequest
|
this.#onRequest
|
||||||
);
|
);
|
||||||
if (Pipeline.isEndpointResult(result, renderContext.route.type)) {
|
if (renderContext.route.type === 'endpoint') {
|
||||||
if (!this.#endpointHandler) {
|
if (!this.#endpointHandler) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'You created a pipeline that does not know how to handle the result coming from an endpoint.'
|
'You created a pipeline that does not know how to handle the result coming from an endpoint.'
|
||||||
|
@ -103,7 +102,7 @@ export class Pipeline {
|
||||||
env: Readonly<Environment>,
|
env: Readonly<Environment>,
|
||||||
mod: Readonly<ComponentInstance>,
|
mod: Readonly<ComponentInstance>,
|
||||||
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||||
): Promise<Response | EndpointCallResult> {
|
): Promise<Response> {
|
||||||
const apiContext = createAPIContext({
|
const apiContext = createAPIContext({
|
||||||
request: renderContext.request,
|
request: renderContext.request,
|
||||||
params: renderContext.params,
|
params: renderContext.params,
|
||||||
|
@ -151,15 +150,4 @@ export class Pipeline {
|
||||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this function
|
|
||||||
*/
|
|
||||||
static isEndpointResult(result: any, routeType: RouteType): result is EndpointCallResult {
|
|
||||||
return !(result instanceof Response) && routeType === 'endpoint';
|
|
||||||
}
|
|
||||||
|
|
||||||
static isResponse(result: any, routeType: RouteType): result is Response {
|
|
||||||
return result instanceof Response && (routeType === 'page' || routeType === 'redirect');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,10 @@ import type {
|
||||||
EndpointHandler,
|
EndpointHandler,
|
||||||
MiddlewareHandler,
|
MiddlewareHandler,
|
||||||
MiddlewareResponseHandler,
|
MiddlewareResponseHandler,
|
||||||
RouteType,
|
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
||||||
import { attachCookiesToResponse } from '../cookies/index.js';
|
import { attachCookiesToResponse } from '../cookies/index.js';
|
||||||
import { callEndpoint, createAPIContext, type EndpointCallResult } from '../endpoint/index.js';
|
import { callEndpoint, createAPIContext } from '../endpoint/index.js';
|
||||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
|
import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
|
||||||
import type { RenderContext } from './context.js';
|
import type { RenderContext } from './context.js';
|
||||||
|
@ -92,7 +91,7 @@ export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
||||||
env: Readonly<Environment>,
|
env: Readonly<Environment>,
|
||||||
mod: Readonly<ComponentInstance>,
|
mod: Readonly<ComponentInstance>,
|
||||||
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||||
): Promise<Response | EndpointCallResult> {
|
): Promise<Response> {
|
||||||
const apiContext = createAPIContext({
|
const apiContext = createAPIContext({
|
||||||
request: renderContext.request,
|
request: renderContext.request,
|
||||||
params: renderContext.params,
|
params: renderContext.params,
|
||||||
|
@ -140,11 +139,3 @@ export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
||||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEndpointResult(result: any, routeType: RouteType): result is EndpointCallResult {
|
|
||||||
return !(result instanceof Response) && routeType === 'endpoint';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isResponse(result: any, routeType: RouteType): result is Response {
|
|
||||||
return result instanceof Response && (routeType === 'page' || routeType === 'redirect');
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Pipeline } from '../core/pipeline.js';
|
import { Pipeline } from '../core/pipeline.js';
|
||||||
import type { AstroConfig, AstroSettings, RouteData } from '../@types/astro';
|
import type { AstroConfig, AstroSettings } from '../@types/astro';
|
||||||
import type { ModuleLoader } from '../core/module-loader';
|
import type { ModuleLoader } from '../core/module-loader';
|
||||||
import type { Environment } from '../core/render';
|
import type { Environment } from '../core/render';
|
||||||
import { createEnvironment, loadRenderer } from '../core/render/index.js';
|
import { createEnvironment, loadRenderer } from '../core/render/index.js';
|
||||||
|
@ -9,15 +9,11 @@ import { isServerLikeOutput } from '../prerender/utils.js';
|
||||||
import type { RuntimeMode, SSRManifest, SSRLoadedRenderer } from '../@types/astro';
|
import type { RuntimeMode, SSRManifest, SSRLoadedRenderer } from '../@types/astro';
|
||||||
import type { LogOptions } from '../core/logger/core';
|
import type { LogOptions } from '../core/logger/core';
|
||||||
import { Logger } from '../core/logger/core.js';
|
import { Logger } from '../core/logger/core.js';
|
||||||
import type { EndpointCallResult } from '../core/endpoint/index.js';
|
|
||||||
import mime from 'mime';
|
|
||||||
import { attachCookiesToResponse } from '../core/cookies/index.js';
|
|
||||||
|
|
||||||
export default class DevPipeline extends Pipeline {
|
export default class DevPipeline extends Pipeline {
|
||||||
#settings: AstroSettings;
|
#settings: AstroSettings;
|
||||||
#loader: ModuleLoader;
|
#loader: ModuleLoader;
|
||||||
#devLogger: Logger;
|
#devLogger: Logger;
|
||||||
#currentMatchedRoute: RouteData | undefined;
|
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
manifest,
|
manifest,
|
||||||
|
@ -38,10 +34,6 @@ export default class DevPipeline extends Pipeline {
|
||||||
this.setEndpointHandler(this.#handleEndpointResult);
|
this.setEndpointHandler(this.#handleEndpointResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentMatchedRoute(route: RouteData) {
|
|
||||||
this.#currentMatchedRoute = route;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearRouteCache() {
|
clearRouteCache() {
|
||||||
this.env.routeCache.clearAll();
|
this.env.routeCache.clearAll();
|
||||||
}
|
}
|
||||||
|
@ -93,36 +85,7 @@ export default class DevPipeline extends Pipeline {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleEndpointResult(_: Request, result: EndpointCallResult): Promise<Response> {
|
async #handleEndpointResult(_: Request, response: Response): Promise<Response> {
|
||||||
if (result.type === 'simple') {
|
|
||||||
if (!this.#currentMatchedRoute) {
|
|
||||||
throw new Error(
|
|
||||||
'In development mode, you must set the current matched route before handling a endpoint.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 =
|
|
||||||
this.#currentMatchedRoute.pathname ||
|
|
||||||
this.#currentMatchedRoute.segments
|
|
||||||
.map((segment) => segment.map((p) => p.content).join(''))
|
|
||||||
.join('/');
|
|
||||||
const computedMimeType = mime.getType(filepath);
|
|
||||||
if (computedMimeType) {
|
|
||||||
contentType = computedMimeType;
|
|
||||||
}
|
|
||||||
const response = new Response(
|
|
||||||
result.encoding !== 'binary' ? Buffer.from(result.body, result.encoding) : result.body,
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': `${contentType};charset=utf-8`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
attachCookiesToResponse(response, result.cookies);
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
return result.response;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,7 +216,6 @@ export async function handleRoute({
|
||||||
if (onRequest) {
|
if (onRequest) {
|
||||||
pipeline.setMiddlewareFunction(onRequest);
|
pipeline.setMiddlewareFunction(onRequest);
|
||||||
}
|
}
|
||||||
pipeline.setCurrentMatchedRoute(route);
|
|
||||||
|
|
||||||
let response = await pipeline.renderRoute(renderContext, mod);
|
let response = await pipeline.renderRoute(renderContext, mod);
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
|
|
12
packages/astro/test/fixtures/non-html-pages/src/pages/about-object.json.ts
vendored
Normal file
12
packages/astro/test/fixtures/non-html-pages/src/pages/about-object.json.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// NOTE: test deprecated object form
|
||||||
|
// Returns the file body for this non-HTML file.
|
||||||
|
// The content type is based off of the extension in the filename,
|
||||||
|
// in this case: about.json.
|
||||||
|
export async function GET() {
|
||||||
|
return {
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Astro',
|
||||||
|
url: 'https://astro.build/',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
// Returns the file body for this non-HTML file.
|
// Returns the file body for this non-HTML file.
|
||||||
// The content type is based off of the extension in the filename,
|
|
||||||
// in this case: about.json.
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
return {
|
const data = JSON.stringify({
|
||||||
body: JSON.stringify({
|
|
||||||
name: 'Astro',
|
name: 'Astro',
|
||||||
url: 'https://astro.build/',
|
url: 'https://astro.build/',
|
||||||
}),
|
})
|
||||||
};
|
return new Response(data, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
18
packages/astro/test/fixtures/non-html-pages/src/pages/placeholder-object.png.ts
vendored
Normal file
18
packages/astro/test/fixtures/non-html-pages/src/pages/placeholder-object.png.ts
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { promises as fs } from 'node:fs';
|
||||||
|
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
|
// NOTE: test deprecated object form
|
||||||
|
export const GET: APIRoute = async function get() {
|
||||||
|
try {
|
||||||
|
// Image is in the public domain. Sourced from
|
||||||
|
// https://en.wikipedia.org/wiki/File:Portrait_placeholder.png
|
||||||
|
const buffer = await fs.readFile('./test/fixtures/non-html-pages/src/images/placeholder.png');
|
||||||
|
return {
|
||||||
|
body: buffer.toString('binary'),
|
||||||
|
encoding: 'binary',
|
||||||
|
} as const;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
throw new Error(`Something went wrong in placeholder.png route!: ${error as string}`);
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,15 +2,12 @@ import { promises as fs } from 'node:fs';
|
||||||
|
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
export const GET: APIRoute = async function get() {
|
export const GET: APIRoute = async function get({ ResponseWithEncoding }) {
|
||||||
try {
|
try {
|
||||||
// Image is in the public domain. Sourced from
|
// Image is in the public domain. Sourced from
|
||||||
// https://en.wikipedia.org/wiki/File:Portrait_placeholder.png
|
// https://en.wikipedia.org/wiki/File:Portrait_placeholder.png
|
||||||
const buffer = await fs.readFile('./test/fixtures/non-html-pages/src/images/placeholder.png');
|
const buffer = await fs.readFile('./test/fixtures/non-html-pages/src/images/placeholder.png');
|
||||||
return {
|
return new ResponseWithEncoding(buffer.toString('binary'), undefined, 'binary')
|
||||||
body: buffer.toString('binary'),
|
|
||||||
encoding: 'binary',
|
|
||||||
} as const;
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
throw new Error(`Something went wrong in placeholder.png route!: ${error as string}`);
|
throw new Error(`Something went wrong in placeholder.png route!: ${error as string}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|
||||||
export function GET() {
|
export function GET() {
|
||||||
return {
|
return new Response('ok')
|
||||||
body: 'ok'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function post({ request }) {
|
export async function post({ request }) {
|
||||||
|
|
10
packages/astro/test/fixtures/ssr-api-route/src/pages/food-object.json.js
vendored
Normal file
10
packages/astro/test/fixtures/ssr-api-route/src/pages/food-object.json.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// NOTE: test deprecated object form
|
||||||
|
export function GET() {
|
||||||
|
return {
|
||||||
|
body: JSON.stringify([
|
||||||
|
{ name: 'lettuce' },
|
||||||
|
{ name: 'broccoli' },
|
||||||
|
{ name: 'pizza' }
|
||||||
|
])
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
|
|
||||||
export function GET() {
|
export function GET() {
|
||||||
return {
|
return new Response(
|
||||||
body: JSON.stringify([
|
JSON.stringify([
|
||||||
{ name: 'lettuce' },
|
{ name: 'lettuce' },
|
||||||
{ name: 'broccoli' },
|
{ name: 'broccoli' },
|
||||||
{ name: 'pizza' }
|
{ name: 'pizza' }
|
||||||
])
|
])
|
||||||
};
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST({ params, request }) {
|
export async function POST({ params, request }) {
|
||||||
|
|
|
@ -15,6 +15,12 @@ describe('Non-HTML Pages', () => {
|
||||||
expect(json).to.have.property('name', 'Astro');
|
expect(json).to.have.property('name', 'Astro');
|
||||||
expect(json).to.have.property('url', 'https://astro.build/');
|
expect(json).to.have.property('url', 'https://astro.build/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should match contents (deprecated object form)', async () => {
|
||||||
|
const json = JSON.parse(await fixture.readFile('/about-object.json'));
|
||||||
|
expect(json).to.have.property('name', 'Astro');
|
||||||
|
expect(json).to.have.property('url', 'https://astro.build/');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('png', () => {
|
describe('png', () => {
|
||||||
|
@ -34,5 +40,22 @@ describe('Non-HTML Pages', () => {
|
||||||
'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'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not have had its encoding mangled (deprecated object form)', async () => {
|
||||||
|
const buffer = await fixture.readFile('/placeholder-object.png', 'base64');
|
||||||
|
|
||||||
|
// Sanity check the first byte
|
||||||
|
const hex = Buffer.from(buffer, 'base64').toString('hex');
|
||||||
|
const firstHexByte = hex.slice(0, 2);
|
||||||
|
// If we accidentally utf8 encode the png, the first byte (in hex) will be 'c2'
|
||||||
|
expect(firstHexByte).to.not.equal('c2');
|
||||||
|
// and if correctly encoded in binary, it should be '89'
|
||||||
|
expect(firstHexByte).to.equal('89');
|
||||||
|
|
||||||
|
// Make sure the whole buffer (in base64) matches this snapshot
|
||||||
|
expect(buffer).to.equal(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAGQAAACWCAYAAAAouC1GAAAD10lEQVR4Xu3ZbW4iMRCE4c1RuP+ZEEfZFZHIAgHGH9Xtsv3m94yx6qHaM+HrfD7//cOfTQJfgNhYfG8EEC8PQMw8AAHELQGz/XCGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbWe6hpxOp6oIL5dL1fWjL54CpBbhXagz4FiDqCCegZxhLEGiIGaAsQPJwrjhuLXFBiQbwrUtFiCjMZzaMhzEBcMFZSiIG4YDyjAQV4zRKENA3DFGoqSDzIIxCgWQgn9eZb6rpILM1o57qyyUNJCZMTLHFyAFI2s5kBXakYWS0hBAymsYDrISRkZLACn/8j5cGfXUFQqyYjuiWwJIY0Out0W0JAxk5XZEtgQQGtKRgOGt6rEV0pAdxlXU2AKks3U0pDPAiNuVKDREIGQNstP5EXGOyBsCSF/lAOnL7/tuRpYgRPUSKhQaIpIBRBSkahlAVEmK1gFEFKRqGUuQHR951e8i0kMdkP6+SUGu29kVxXJkAUJD+hMQrUBDREGqlgFElaRgHRXGdSsc6oAIEjBbgoYAUpfAbu8i1g3Z7V1EiRFyqANSN02er5Y/Zd0+YJexNUVDdmmJGiNsZAHSPrbCRtYOKFM1ZHWQCIzQkbX64Q5I+1iW3xmFkdKQFUcXIPLvePuCkRhpDVmpJcuArIASjZHakNmfujIwAKk4SpYFmXF0ZWEMachsoysTYyjIDE3JxhgO4owyAsMCxBFlFIYNiBPKSAxAnh57R2PYgLj9/j4SJvQXw5L3LjeM+z2PgBkG4gzx/EXKhEkHmQliRFvSQGaFyEZJAVkB4wYTPb7CQVbCyEAJA1kRImN8hYCsjhHZFDnILhhRKICUvL0eXKM86KUgu7Uj4kyRgeyMoRxfEhAw/neld3x1g4Dx+4DpQQFEcKi/WqIVpQuEdrzXTAcB47haLSjNDQHkGOR6RS1KEwgYZRgtj8PVIGDUYdS2BJD6fJvuKB1dVSC0o8ni56YSFED6Mq66WwpCO6qyf3vxEUpxQwAxAgFDg1HyGFzUEECMQMDQYhy15LAhgBiBgBGD8ent/WNDAIkDeYcCSGzmH1d/9U7yFoR25Eg9owCSk3vxmzsgM4AwrnKV7sfWy4YAAkhuAmaf9rEhtCNfC5D8zA8/8Yby6wyhIYfZhVwASEis7Yu+BKEd7YH23glIb4IB919RHs4QGhKQcsWSgFSElXEpIBkpV3zGAwjjqiK5oEsBCQq2Z9l/4WuAC09sfQEAAAAASUVORK5CYII='
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,6 +29,15 @@ describe('API routes in SSR', () => {
|
||||||
const request = new Request('http://example.com/food.json');
|
const request = new Request('http://example.com/food.json');
|
||||||
const response = await app.render(request);
|
const response = await app.render(request);
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.length).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can load the API route too (deprecated object form)', async () => {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/food-object.json');
|
||||||
|
const response = await app.render(request);
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
|
expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
|
||||||
expect(response.headers.get('Content-Length')).to.not.be.empty;
|
expect(response.headers.get('Content-Length')).to.not.be.empty;
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
|
@ -87,8 +96,8 @@ describe('API routes in SSR', () => {
|
||||||
expect(res.status).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Infer content type with charset for { body } shorthand', async () => {
|
it('Infer content type with charset for { body } shorthand (deprecated object form)', async () => {
|
||||||
const response = await fixture.fetch('/food.json', {
|
const response = await fixture.fetch('/food-object.json', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
|
expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');
|
||||||
|
|
Loading…
Add table
Reference in a new issue