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

Remove support for simple objects in endpoints #9181

Merged
merged 5 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
9 changes: 9 additions & 0 deletions .changeset/clever-beds-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'astro': major
---

Removes support for returning simple objects from endpoints (deprecated since Astro 3.0). You should return a `Response` instead.

`ResponseWithEncoding` is also removed. You can refactor the code to return a response with an array buffer instead, which is encoding agnostic.

The types for middlewares are also cleaned up. To type a middleware function, you should now use `MiddlewareHandler` instead of `MiddlewareResponseHandler`. If you used `defineMiddleware()` to type the function, no changes are needed.
matthewp marked this conversation as resolved.
Show resolved Hide resolved
34 changes: 8 additions & 26 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import type { AstroConfigType } from '../core/config/index.js';
import type { AstroTimer } from '../core/config/timer.js';
import type { TSConfig } from '../core/config/tsconfig.js';
import type { AstroCookies } from '../core/cookies/index.js';
import type { ResponseWithEncoding } from '../core/endpoint/index.js';
import type { AstroIntegrationLogger, Logger, LoggerLevel } from '../core/logger/core.js';
import type { AstroDevOverlay, DevOverlayCanvas } from '../runtime/client/dev-overlay/overlay.js';
import type { DevOverlayHighlight } from '../runtime/client/dev-overlay/ui-library/highlight.js';
Expand Down Expand Up @@ -2005,8 +2004,6 @@ export interface AstroAdapter {
supportedAstroFeatures: AstroFeatureMap;
}

type Body = string;

export type ValidRedirectStatus = 300 | 301 | 302 | 303 | 304 | 307 | 308;

// Shared types between `Astro` global and API context object
Expand Down Expand Up @@ -2163,7 +2160,6 @@ export interface APIContext<
* ```
*/
locals: App.Locals;
ResponseWithEncoding: typeof ResponseWithEncoding;

/**
* Available only when `experimental.i18n` enabled and in SSR.
Expand Down Expand Up @@ -2199,22 +2195,12 @@ export interface APIContext<
currentLocale: string | undefined;
}

export type EndpointOutput =
| {
body: Body;
encoding?: BufferEncoding;
}
| {
body: Uint8Array;
encoding: 'binary';
};

export type APIRoute<Props extends Record<string, any> = Record<string, any>> = (
context: APIContext<Props>
) => EndpointOutput | Response | Promise<EndpointOutput | Response>;
) => Response | Promise<Response>;

export interface EndpointHandler {
[method: string]: APIRoute | ((params: Params, request: Request) => EndpointOutput | Response);
[method: string]: APIRoute | ((params: Params, request: Request) => Response);
}

export type Props = Record<string, unknown>;
Expand Down Expand Up @@ -2319,20 +2305,16 @@ export interface AstroIntegration {
};
}

export type MiddlewareNext<R> = () => Promise<R>;
export type MiddlewareHandler<R> = (
export type MiddlewareNext = () => Promise<Response>;
export type MiddlewareHandler = (
context: APIContext,
next: MiddlewareNext<R>
) => Promise<R> | R | Promise<void> | void;

export type MiddlewareResponseHandler = MiddlewareHandler<Response>;
export type MiddlewareEndpointHandler = MiddlewareHandler<Response | EndpointOutput>;
export type MiddlewareNextResponse = MiddlewareNext<Response>;
next: MiddlewareNext
) => Promise<Response> | Response | Promise<void> | void;

// NOTE: when updating this file with other functions,
// remember to update `plugin-page.ts` too, to add that function as a no-op function.
export type AstroMiddlewareInstance<R> = {
onRequest?: MiddlewareHandler<R>;
export type AstroMiddlewareInstance = {
onRequest?: MiddlewareHandler;
};

export type AstroIntegrationMiddleware = {
Expand Down
13 changes: 5 additions & 8 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
EndpointHandler,
ManifestData,
MiddlewareEndpointHandler,
RouteData,
SSRElement,
SSRManifest,
Expand Down Expand Up @@ -181,16 +180,14 @@ export class App {
);
if (i18nMiddleware) {
if (mod.onRequest) {
this.#pipeline.setMiddlewareFunction(
sequence(i18nMiddleware, mod.onRequest as MiddlewareEndpointHandler)
);
this.#pipeline.setMiddlewareFunction(sequence(i18nMiddleware, mod.onRequest));
} else {
this.#pipeline.setMiddlewareFunction(i18nMiddleware);
}
this.#pipeline.onBeforeRenderRoute(i18nPipelineHook);
} else {
if (mod.onRequest) {
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
this.#pipeline.setMiddlewareFunction(mod.onRequest);
}
}
response = await this.#pipeline.renderRoute(renderContext, pageModule);
Expand Down Expand Up @@ -322,7 +319,7 @@ export class App {
);
const page = (await mod.page()) as any;
if (skipMiddleware === false && mod.onRequest) {
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
this.#pipeline.setMiddlewareFunction(mod.onRequest);
}
if (skipMiddleware) {
// make sure middleware set by other requests is cleared out
Expand Down Expand Up @@ -367,8 +364,8 @@ export class App {
const status = override?.status
? override.status
: oldResponse.status === 200
? newResponse.status
: oldResponse.status;
? newResponse.status
: oldResponse.status;

return new Response(newResponse.body, {
status,
Expand Down
11 changes: 3 additions & 8 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
AstroSettings,
ComponentInstance,
GetStaticPathsItem,
MiddlewareEndpointHandler,
RouteData,
RouteType,
SSRError,
Expand Down Expand Up @@ -269,15 +268,13 @@ async function generatePage(
);
if (config.experimental.i18n && i18nMiddleware) {
if (onRequest) {
pipeline.setMiddlewareFunction(
sequence(i18nMiddleware, onRequest as MiddlewareEndpointHandler)
);
pipeline.setMiddlewareFunction(sequence(i18nMiddleware, onRequest));
} else {
pipeline.setMiddlewareFunction(i18nMiddleware);
}
pipeline.onBeforeRenderRoute(i18nPipelineHook);
} else if (onRequest) {
pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler);
pipeline.setMiddlewareFunction(onRequest);
}
if (!pageModulePromise) {
throw new Error(
Expand Down Expand Up @@ -560,7 +557,6 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
});

let body: string | Uint8Array;
let encoding: BufferEncoding | undefined;

let response: Response;
try {
Expand Down Expand Up @@ -603,15 +599,14 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
// If there's no body, do nothing
if (!response.body) return;
body = Buffer.from(await response.arrayBuffer());
encoding = (response.headers.get('X-Astro-Encoding') as BufferEncoding | null) ?? 'utf-8';
}

const outFolder = getOutFolder(pipeline.getConfig(), pathname, route.type);
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route.type);
route.distURL = outFile;

await fs.promises.mkdir(outFolder, { recursive: true });
await fs.promises.writeFile(outFile, body, encoding);
await fs.promises.writeFile(outFile, body);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface SinglePageBuiltModule {
/**
* The `onRequest` hook exported by the middleware
*/
onRequest?: MiddlewareHandler<unknown>;
onRequest?: MiddlewareHandler;
renderers: SSRLoadedRenderer[];
}

Expand Down
142 changes: 6 additions & 136 deletions packages/astro/src/core/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import mime from 'mime';
import type {
APIContext,
EndpointHandler,
EndpointOutput,
MiddlewareEndpointHandler,
MiddlewareHandler,
Params,
} from '../../@types/astro.js';
import type { APIContext, EndpointHandler, MiddlewareHandler, Params } from '../../@types/astro.js';
import { renderEndpoint } from '../../runtime/server/index.js';
import { ASTRO_VERSION } from '../constants.js';
import { AstroCookies, attachCookiesToResponse } from '../cookies/index.js';
Expand All @@ -19,8 +11,6 @@ import {
} from '../render/context.js';
import { type Environment, type RenderContext } from '../render/index.js';

const encoder = new TextEncoder();

const clientAddressSymbol = Symbol.for('astro.clientAddress');
const clientLocalsSymbol = Symbol.for('astro.locals');

Expand Down Expand Up @@ -69,7 +59,6 @@ export function createAPIContext({
},
});
},
ResponseWithEncoding,
get preferredLocale(): string | undefined {
if (preferredLocale) {
return preferredLocale;
Expand Down Expand Up @@ -143,36 +132,11 @@ export function createAPIContext({
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(
mod: EndpointHandler,
env: Environment,
ctx: RenderContext,
onRequest: MiddlewareHandler<MiddlewareResult> | undefined
onRequest: MiddlewareHandler | undefined
): Promise<Response> {
const context = createAPIContext({
request: ctx.request,
Expand All @@ -187,107 +151,13 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>

let response;
if (onRequest) {
response = await callMiddleware<Response | EndpointOutput>(
env.logger,
onRequest as MiddlewareEndpointHandler,
context,
async () => {
return await renderEndpoint(mod, context, env.ssr, env.logger);
}
);
response = await callMiddleware(onRequest, context, async () => {
return await renderEndpoint(mod, context, env.ssr, env.logger);
});
} else {
response = await renderEndpoint(mod, context, env.ssr, env.logger);
}

const isEndpointSSR = env.ssr && !ctx.route?.prerender;

if (response instanceof Response) {
if (isEndpointSSR && response.headers.get('X-Astro-Encoding')) {
env.logger.warn(
null,
'`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);
return response;
}

// The endpoint returned a simple object, convert it to a Response

// TODO: Remove in Astro 4.0
env.logger.warn(
null,
`${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')) {
env.logger.warn(
null,
'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) {
env.logger.warn(
null,
'`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.'
);
}
}

let body: BodyInit;
const headers = new Headers();

// 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;
}
Loading
Loading