Skip to content

Commit

Permalink
Remove support for simple objects in endpoints (#9181)
Browse files Browse the repository at this point in the history
* Deprecate simple object from endpoints

* Update changeset

* Add missing Response return

Co-authored-by: Happydev <81974850+MoustaphaDev@users.noreply.github.com>

* Update .changeset/clever-beds-notice.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

---------

Co-authored-by: Happydev <81974850+MoustaphaDev@users.noreply.github.com>
Co-authored-by: Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
4 people committed Nov 27, 2023
1 parent 37697a2 commit cdabf6e
Show file tree
Hide file tree
Showing 56 changed files with 206 additions and 529 deletions.
9 changes: 9 additions & 0 deletions .changeset/clever-beds-notice.md
@@ -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 have also been revised. 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.
34 changes: 8 additions & 26 deletions packages/astro/src/@types/astro.ts
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
@@ -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
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
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
@@ -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;
}

0 comments on commit cdabf6e

Please sign in to comment.