summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/yellow-tips-cover.md26
-rw-r--r--packages/astro/src/@types/astro.ts4
-rw-r--r--packages/astro/src/core/app/index.ts2
-rw-r--r--packages/astro/src/core/app/ssrPipeline.ts34
-rw-r--r--packages/astro/src/core/build/buildPipeline.ts54
-rw-r--r--packages/astro/src/core/build/generate.ts12
-rw-r--r--packages/astro/src/core/endpoint/index.ts125
-rw-r--r--packages/astro/src/core/pipeline.ts20
-rw-r--r--packages/astro/src/core/render/core.ts13
-rw-r--r--packages/astro/src/vite-plugin-astro-server/devPipeline.ts43
-rw-r--r--packages/astro/src/vite-plugin-astro-server/route.ts1
-rw-r--r--packages/astro/test/fixtures/non-html-pages/src/pages/about-object.json.ts12
-rw-r--r--packages/astro/test/fixtures/non-html-pages/src/pages/about.json.ts17
-rw-r--r--packages/astro/test/fixtures/non-html-pages/src/pages/placeholder-object.png.ts18
-rw-r--r--packages/astro/test/fixtures/non-html-pages/src/pages/placeholder.png.ts7
-rw-r--r--packages/astro/test/fixtures/ssr-api-route/src/pages/binary.js4
-rw-r--r--packages/astro/test/fixtures/ssr-api-route/src/pages/food-object.json.js10
-rw-r--r--packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js6
-rw-r--r--packages/astro/test/non-html-pages.test.js23
-rw-r--r--packages/astro/test/ssr-api-route.test.js13
20 files changed, 243 insertions, 201 deletions
diff --git a/.changeset/yellow-tips-cover.md b/.changeset/yellow-tips-cover.md
new file mode 100644
index 000000000..fb373374e
--- /dev/null
+++ b/.changeset/yellow-tips-cover.md
@@ -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');
+}
+```
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 1198b26be..1d8875b4b 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -23,6 +23,7 @@ import type { LogOptions, LoggerLevel } from '../core/logger/core';
import type { AstroIntegrationLogger } from '../core/logger/core';
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
+import type { ResponseWithEncoding } from '../core/endpoint/index.js';
export type {
MarkdownHeading,
@@ -1963,12 +1964,13 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
* ```
*/
locals: App.Locals;
+ ResponseWithEncoding: typeof ResponseWithEncoding;
}
export type EndpointOutput =
| {
body: Body;
- encoding?: Exclude<BufferEncoding, 'binary'>;
+ encoding?: BufferEncoding;
}
| {
body: Uint8Array;
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index 86b9c9d41..95abac957 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -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)) {
return this.#renderError(request, {
response,
diff --git a/packages/astro/src/core/app/ssrPipeline.ts b/packages/astro/src/core/app/ssrPipeline.ts
index 5f135e42d..6ee135197 100644
--- a/packages/astro/src/core/app/ssrPipeline.ts
+++ b/packages/astro/src/core/app/ssrPipeline.ts
@@ -1,7 +1,4 @@
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';
/**
@@ -16,39 +13,16 @@ export class EndpointNotFoundError extends Error {
}
export class SSRRoutePipeline extends Pipeline {
- #encoder = new TextEncoder();
-
constructor(env: Environment) {
super(env);
this.setEndpointHandler(this.#ssrEndpointHandler);
}
// This function is responsible for handling the result coming from an endpoint.
- async #ssrEndpointHandler(request: Request, response: EndpointCallResult): Promise<Response> {
- if (response.type === 'response') {
- if (response.response.headers.get('X-Astro-Response') === 'Not-Found') {
- 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;
+ async #ssrEndpointHandler(request: Request, response: Response): Promise<Response> {
+ if (response.headers.get('X-Astro-Response') === 'Not-Found') {
+ throw new EndpointNotFoundError(response);
}
+ return response
}
}
diff --git a/packages/astro/src/core/build/buildPipeline.ts b/packages/astro/src/core/build/buildPipeline.ts
index 4ebf48a9a..52d694077 100644
--- a/packages/astro/src/core/build/buildPipeline.ts
+++ b/packages/astro/src/core/build/buildPipeline.ts
@@ -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 { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
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 type { EndpointCallResult } from '../endpoint';
import { createEnvironment } from '../render/index.js';
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.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.
@@ -20,10 +18,6 @@ export class BuildPipeline extends Pipeline {
#internals: BuildInternals;
#staticBuildOptions: StaticBuildOptions;
#manifest: SSRManifest;
- #currentEndpointBody?: {
- body: string | Uint8Array;
- encoding: BufferEncoding;
- };
constructor(
staticBuildOptions: StaticBuildOptions,
@@ -163,49 +157,7 @@ export class BuildPipeline extends Pipeline {
return pages;
}
- async #handleEndpointResult(request: Request, response: EndpointCallResult): Promise<Response> {
- if (response.type === '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 };
- }
+ async #handleEndpointResult(_: Request, response: Response): Promise<Response> {
+ return response;
}
}
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index a3bc80295..7545fa577 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -567,20 +567,16 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
} else {
// If there's no body, do nothing
if (!response.body) return;
- const result = await pipeline.computeBodyAndEncoding(renderContext.route.type, response);
- body = result.body;
- encoding = result.encoding;
+ body = Buffer.from(await response.arrayBuffer());
+ encoding = (response.headers.get('X-Astro-Encoding') as BufferEncoding | null) ?? 'utf-8';
}
const outFolder = getOutFolder(pipeline.getConfig(), pathname, pageData.route.type);
const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, pageData.route.type);
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.writeFile(outFile, body, encoding ?? 'utf-8');
+ await fs.promises.writeFile(outFile, body, encoding);
}
/**
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index 9298e7cbe..9c3b57133 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -6,6 +6,7 @@ import type {
MiddlewareHandler,
Params,
} from '../../@types/astro';
+import mime from 'mime';
import type { Environment, RenderContext } from '../render/index';
import { renderEndpoint } from '../../runtime/server/index.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 { callMiddleware } from '../middleware/callMiddleware.js';
+const encoder = new TextEncoder();
+
const clientAddressSymbol = Symbol.for('astro.clientAddress');
const clientLocalsSymbol = Symbol.for('astro.locals');
-export type EndpointCallResult =
- | (EndpointOutput & {
- type: 'simple';
- cookies: AstroCookies;
- })
- | {
- type: 'response';
- response: Response;
- };
-
type CreateAPIContext = {
request: Request;
params: Params;
@@ -62,6 +55,7 @@ export function createAPIContext({
},
});
},
+ ResponseWithEncoding,
url: new URL(request.url),
get clientAddress() {
if (!(clientAddressSymbol in request)) {
@@ -96,12 +90,37 @@ 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>(
mod: EndpointHandler,
env: Environment,
ctx: RenderContext,
onRequest?: MiddlewareHandler<MiddlewareResult> | undefined
-): Promise<EndpointCallResult> {
+): Promise<Response> {
const context = createAPIContext({
request: ctx.request,
params: ctx.params,
@@ -124,15 +143,30 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
response = await renderEndpoint(mod, context, env.ssr, env.logging);
}
+ const isEndpointSSR = env.ssr && !ctx.route?.prerender;
+
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);
- return {
- type: 'response',
- response,
- };
+ return 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')) {
warn(
env.logging,
@@ -150,9 +184,58 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
}
}
- return {
- ...response,
- type: 'simple',
- cookies: context.cookies,
- };
+ 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;
}
diff --git a/packages/astro/src/core/pipeline.ts b/packages/astro/src/core/pipeline.ts
index 42ed7d5da..19e1ef82b 100644
--- a/packages/astro/src/core/pipeline.ts
+++ b/packages/astro/src/core/pipeline.ts
@@ -1,11 +1,10 @@
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 {
MiddlewareHandler,
MiddlewareResponseHandler,
ComponentInstance,
MiddlewareEndpointHandler,
- RouteType,
EndpointHandler,
} from '../@types/astro';
import { callMiddleware } from './middleware/callMiddleware.js';
@@ -13,7 +12,7 @@ import { renderPage } from './render/core.js';
type EndpointResultHandler = (
originalRequest: Request,
- result: EndpointCallResult
+ result: Response
) => Promise<Response> | Response;
/**
@@ -76,7 +75,7 @@ export class Pipeline {
componentInstance,
this.#onRequest
);
- if (Pipeline.isEndpointResult(result, renderContext.route.type)) {
+ if (renderContext.route.type === 'endpoint') {
if (!this.#endpointHandler) {
throw new Error(
'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>,
mod: Readonly<ComponentInstance>,
onRequest?: MiddlewareHandler<MiddlewareReturnType>
- ): Promise<Response | EndpointCallResult> {
+ ): Promise<Response> {
const apiContext = createAPIContext({
request: renderContext.request,
params: renderContext.params,
@@ -151,15 +150,4 @@ export class Pipeline {
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');
- }
}
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index 9de046278..24c411e0b 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -4,11 +4,10 @@ import type {
EndpointHandler,
MiddlewareHandler,
MiddlewareResponseHandler,
- RouteType,
} from '../../@types/astro';
import { renderPage as runtimeRenderPage } from '../../runtime/server/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 { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
import type { RenderContext } from './context.js';
@@ -92,7 +91,7 @@ export async function tryRenderRoute<MiddlewareReturnType = Response>(
env: Readonly<Environment>,
mod: Readonly<ComponentInstance>,
onRequest?: MiddlewareHandler<MiddlewareReturnType>
-): Promise<Response | EndpointCallResult> {
+): Promise<Response> {
const apiContext = createAPIContext({
request: renderContext.request,
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}]`);
}
}
-
-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');
-}
diff --git a/packages/astro/src/vite-plugin-astro-server/devPipeline.ts b/packages/astro/src/vite-plugin-astro-server/devPipeline.ts
index eae6cc1c6..a17472895 100644
--- a/packages/astro/src/vite-plugin-astro-server/devPipeline.ts
+++ b/packages/astro/src/vite-plugin-astro-server/devPipeline.ts
@@ -1,5 +1,5 @@
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 { Environment } from '../core/render';
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 { LogOptions } from '../core/logger/core';
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 {
#settings: AstroSettings;
#loader: ModuleLoader;
#devLogger: Logger;
- #currentMatchedRoute: RouteData | undefined;
constructor({
manifest,
@@ -38,10 +34,6 @@ export default class DevPipeline extends Pipeline {
this.setEndpointHandler(this.#handleEndpointResult);
}
- setCurrentMatchedRoute(route: RouteData) {
- this.#currentMatchedRoute = route;
- }
-
clearRouteCache() {
this.env.routeCache.clearAll();
}
@@ -93,36 +85,7 @@ export default class DevPipeline extends Pipeline {
});
}
- async #handleEndpointResult(_: Request, result: EndpointCallResult): 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 result.response;
+ async #handleEndpointResult(_: Request, response: Response): Promise<Response> {
+ return response;
}
}
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index ae4318092..beab313d4 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -216,7 +216,6 @@ export async function handleRoute({
if (onRequest) {
pipeline.setMiddlewareFunction(onRequest);
}
- pipeline.setCurrentMatchedRoute(route);
let response = await pipeline.renderRoute(renderContext, mod);
if (response.status === 404) {
diff --git a/packages/astro/test/fixtures/non-html-pages/src/pages/about-object.json.ts b/packages/astro/test/fixtures/non-html-pages/src/pages/about-object.json.ts
new file mode 100644
index 000000000..3936cbe85
--- /dev/null
+++ b/packages/astro/test/fixtures/non-html-pages/src/pages/about-object.json.ts
@@ -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/',
+ }),
+ };
+}
diff --git a/packages/astro/test/fixtures/non-html-pages/src/pages/about.json.ts b/packages/astro/test/fixtures/non-html-pages/src/pages/about.json.ts
index 0c3ec18ea..817446fb5 100644
--- a/packages/astro/test/fixtures/non-html-pages/src/pages/about.json.ts
+++ b/packages/astro/test/fixtures/non-html-pages/src/pages/about.json.ts
@@ -1,11 +1,12 @@
// 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/',
- }),
- };
+ const data = JSON.stringify({
+ name: 'Astro',
+ url: 'https://astro.build/',
+ })
+ return new Response(data, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
}
diff --git a/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder-object.png.ts b/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder-object.png.ts
new file mode 100644
index 000000000..114159004
--- /dev/null
+++ b/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder-object.png.ts
@@ -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}`);
+ }
+};
diff --git a/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder.png.ts b/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder.png.ts
index 3ee26f0bf..50c6b877b 100644
--- a/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder.png.ts
+++ b/packages/astro/test/fixtures/non-html-pages/src/pages/placeholder.png.ts
@@ -2,15 +2,12 @@ import { promises as fs } from 'node:fs';
import type { APIRoute } from 'astro';
-export const GET: APIRoute = async function get() {
+export const GET: APIRoute = async function get({ ResponseWithEncoding }) {
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;
+ return new ResponseWithEncoding(buffer.toString('binary'), undefined, 'binary')
} catch (error: unknown) {
throw new Error(`Something went wrong in placeholder.png route!: ${error as string}`);
}
diff --git a/packages/astro/test/fixtures/ssr-api-route/src/pages/binary.js b/packages/astro/test/fixtures/ssr-api-route/src/pages/binary.js
index 407c45666..796bf4616 100644
--- a/packages/astro/test/fixtures/ssr-api-route/src/pages/binary.js
+++ b/packages/astro/test/fixtures/ssr-api-route/src/pages/binary.js
@@ -1,9 +1,7 @@
import fs from 'node:fs';
export function GET() {
- return {
- body: 'ok'
- };
+ return new Response('ok')
}
export async function post({ request }) {
diff --git a/packages/astro/test/fixtures/ssr-api-route/src/pages/food-object.json.js b/packages/astro/test/fixtures/ssr-api-route/src/pages/food-object.json.js
new file mode 100644
index 000000000..7992e697a
--- /dev/null
+++ b/packages/astro/test/fixtures/ssr-api-route/src/pages/food-object.json.js
@@ -0,0 +1,10 @@
+// NOTE: test deprecated object form
+export function GET() {
+ return {
+ body: JSON.stringify([
+ { name: 'lettuce' },
+ { name: 'broccoli' },
+ { name: 'pizza' }
+ ])
+ };
+}
diff --git a/packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js b/packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js
index f4021c9e5..2d6fb6d1b 100644
--- a/packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js
+++ b/packages/astro/test/fixtures/ssr-api-route/src/pages/food.json.js
@@ -1,12 +1,12 @@
export function GET() {
- return {
- body: JSON.stringify([
+ return new Response(
+ JSON.stringify([
{ name: 'lettuce' },
{ name: 'broccoli' },
{ name: 'pizza' }
])
- };
+ )
}
export async function POST({ params, request }) {
diff --git a/packages/astro/test/non-html-pages.test.js b/packages/astro/test/non-html-pages.test.js
index e1b89ee6a..3e873032e 100644
--- a/packages/astro/test/non-html-pages.test.js
+++ b/packages/astro/test/non-html-pages.test.js
@@ -15,6 +15,12 @@ describe('Non-HTML Pages', () => {
expect(json).to.have.property('name', 'Astro');
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', () => {
@@ -34,5 +40,22 @@ describe('Non-HTML Pages', () => {
'iVBORw0KGgoAAAANSUhEUgAAAGQAAACWCAYAAAAouC1GAAAD10lEQVR4Xu3ZbW4iMRCE4c1RuP+ZEEfZFZHIAgHGH9Xtsv3m94yx6qHaM+HrfD7//cOfTQJfgNhYfG8EEC8PQMw8AAHELQGz/XCGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbYeGAGKWgNl2aAggZgmYbWe6hpxOp6oIL5dL1fWjL54CpBbhXagz4FiDqCCegZxhLEGiIGaAsQPJwrjhuLXFBiQbwrUtFiCjMZzaMhzEBcMFZSiIG4YDyjAQV4zRKENA3DFGoqSDzIIxCgWQgn9eZb6rpILM1o57qyyUNJCZMTLHFyAFI2s5kBXakYWS0hBAymsYDrISRkZLACn/8j5cGfXUFQqyYjuiWwJIY0Out0W0JAxk5XZEtgQQGtKRgOGt6rEV0pAdxlXU2AKks3U0pDPAiNuVKDREIGQNstP5EXGOyBsCSF/lAOnL7/tuRpYgRPUSKhQaIpIBRBSkahlAVEmK1gFEFKRqGUuQHR951e8i0kMdkP6+SUGu29kVxXJkAUJD+hMQrUBDREGqlgFElaRgHRXGdSsc6oAIEjBbgoYAUpfAbu8i1g3Z7V1EiRFyqANSN02er5Y/Zd0+YJexNUVDdmmJGiNsZAHSPrbCRtYOKFM1ZHWQCIzQkbX64Q5I+1iW3xmFkdKQFUcXIPLvePuCkRhpDVmpJcuArIASjZHakNmfujIwAKk4SpYFmXF0ZWEMachsoysTYyjIDE3JxhgO4owyAsMCxBFlFIYNiBPKSAxAnh57R2PYgLj9/j4SJvQXw5L3LjeM+z2PgBkG4gzx/EXKhEkHmQliRFvSQGaFyEZJAVkB4wYTPb7CQVbCyEAJA1kRImN8hYCsjhHZFDnILhhRKICUvL0eXKM86KUgu7Uj4kyRgeyMoRxfEhAw/neld3x1g4Dx+4DpQQFEcKi/WqIVpQuEdrzXTAcB47haLSjNDQHkGOR6RS1KEwgYZRgtj8PVIGDUYdS2BJD6fJvuKB1dVSC0o8ni56YSFED6Mq66WwpCO6qyf3vxEUpxQwAxAgFDg1HyGFzUEECMQMDQYhy15LAhgBiBgBGD8ent/WNDAIkDeYcCSGzmH1d/9U7yFoR25Eg9owCSk3vxmzsgM4AwrnKV7sfWy4YAAkhuAmaf9rEhtCNfC5D8zA8/8Yby6wyhIYfZhVwASEis7Yu+BKEd7YH23glIb4IB919RHs4QGhKQcsWSgFSElXEpIBkpV3zGAwjjqiK5oEsBCQq2Z9l/4WuAC09sfQEAAAAASUVORK5CYII='
);
});
+
+ 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='
+ );
+ });
});
});
diff --git a/packages/astro/test/ssr-api-route.test.js b/packages/astro/test/ssr-api-route.test.js
index 899404b1e..fbaafc822 100644
--- a/packages/astro/test/ssr-api-route.test.js
+++ b/packages/astro/test/ssr-api-route.test.js
@@ -29,6 +29,15 @@ describe('API routes in SSR', () => {
const request = new Request('http://example.com/food.json');
const response = await app.render(request);
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-Length')).to.not.be.empty;
const body = await response.json();
@@ -87,8 +96,8 @@ describe('API routes in SSR', () => {
expect(res.status).to.equal(200);
});
- it('Infer content type with charset for { body } shorthand', async () => {
- const response = await fixture.fetch('/food.json', {
+ it('Infer content type with charset for { body } shorthand (deprecated object form)', async () => {
+ const response = await fixture.fetch('/food-object.json', {
method: 'GET',
});
expect(response.headers.get('Content-Type')).to.equal('application/json;charset=utf-8');