mirror of
https://github.com/withastro/astro.git
synced 2024-12-23 21:53:55 -05:00
Implement the Astro.response RFC (#3289)
* Implement the Astro.response RFC * Adds a changeset * Mark Astro.response.headers as readonly
This commit is contained in:
parent
d0a4064d80
commit
61e1a267a8
19 changed files with 177 additions and 15 deletions
5
.changeset/angry-rats-turn.md
Normal file
5
.changeset/angry-rats-turn.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Implements the Astro.response RFC
|
|
@ -131,6 +131,18 @@ export interface AstroGlobal extends AstroGlobalPartial {
|
|||
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#astrorequest)
|
||||
*/
|
||||
request: Request;
|
||||
/** Information about the outgoing response. This is a standard [ResponseInit](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#init) object
|
||||
*
|
||||
* For example, to change the status code you can set a different status on this object:
|
||||
* ```typescript
|
||||
* Astro.response.status = 404;
|
||||
* ```
|
||||
*
|
||||
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#astroresponse)
|
||||
*/
|
||||
response: ResponseInit & {
|
||||
readonly headers: Headers
|
||||
};
|
||||
/** Redirect to another page (**SSR Only**)
|
||||
*
|
||||
* Example usage:
|
||||
|
@ -1031,5 +1043,6 @@ export interface SSRResult {
|
|||
slots: Record<string, any> | null
|
||||
): AstroGlobal;
|
||||
resolve: (s: string) => Promise<string>;
|
||||
response: ResponseInit;
|
||||
_metadata: SSRMetadata;
|
||||
}
|
||||
|
|
|
@ -108,14 +108,12 @@ export class App {
|
|||
}
|
||||
|
||||
let html = result.html;
|
||||
let init = result.response;
|
||||
let headers = init.headers as Headers;
|
||||
let bytes = this.#encoder.encode(html);
|
||||
return new Response(bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
'Content-Length': bytes.byteLength.toString(),
|
||||
},
|
||||
});
|
||||
headers.set('Content-Type', 'text/html');
|
||||
headers.set('Content-Length', bytes.byteLength.toString())
|
||||
return new Response(bytes, init);
|
||||
}
|
||||
|
||||
async #callEndpoint(
|
||||
|
|
|
@ -84,7 +84,7 @@ export interface RenderOptions {
|
|||
|
||||
export async function render(
|
||||
opts: RenderOptions
|
||||
): Promise<{ type: 'html'; html: string } | { type: 'response'; response: Response }> {
|
||||
): Promise<{ type: 'html'; html: string, response: ResponseInit; } | { type: 'response'; response: Response }> {
|
||||
const {
|
||||
links,
|
||||
logging,
|
||||
|
@ -162,5 +162,6 @@ export async function render(
|
|||
return {
|
||||
type: 'html',
|
||||
html,
|
||||
response: result.response
|
||||
};
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ export interface SSROptions {
|
|||
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
||||
|
||||
export type RenderResponse =
|
||||
| { type: 'html'; html: string }
|
||||
| { type: 'html'; html: string, response: ResponseInit }
|
||||
| { type: 'response'; response: Response };
|
||||
|
||||
const svelteStylesRE = /svelte\?svelte&type=style/;
|
||||
|
@ -199,6 +199,7 @@ export async function render(
|
|||
return {
|
||||
type: 'html',
|
||||
html,
|
||||
response: content.response,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,18 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
|
||||
const url = new URL(request.url);
|
||||
const canonicalURL = createCanonicalURL('.' + pathname, site ?? url.origin);
|
||||
const response: ResponseInit = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers(),
|
||||
};
|
||||
|
||||
// Make headers be read-only
|
||||
Object.defineProperty(response, 'headers', {
|
||||
value: response.headers,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
// Create the result object that will be passed into the render function.
|
||||
// This object starts here as an empty shell (not yet the result) but then
|
||||
|
@ -168,6 +180,7 @@ ${extra}`
|
|||
// Intentionally return an empty string so that it is not relied upon.
|
||||
return '';
|
||||
},
|
||||
response,
|
||||
slots: astroSlots,
|
||||
} as unknown as AstroGlobal;
|
||||
|
||||
|
@ -202,6 +215,7 @@ ${extra}`
|
|||
renderers,
|
||||
pathname,
|
||||
},
|
||||
response
|
||||
};
|
||||
|
||||
return result;
|
||||
|
|
|
@ -94,8 +94,13 @@ async function writeSSRResult(
|
|||
return;
|
||||
}
|
||||
|
||||
const { html } = result;
|
||||
writeHtmlResponse(res, statusCode, html);
|
||||
const { html, response: init } = result;
|
||||
const headers = init.headers as Headers;
|
||||
|
||||
headers.set('Content-Type', 'text/html; charset=utf-8');
|
||||
headers.set('Content-Length', Buffer.byteLength(html, 'utf-8').toString());
|
||||
|
||||
return writeWebResponse(res, new Response(html, init));
|
||||
}
|
||||
|
||||
async function handle404Response(
|
||||
|
|
9
packages/astro/test/fixtures/ssr-response/src/components/OverwriteHeader.astro
vendored
Normal file
9
packages/astro/test/fixtures/ssr-response/src/components/OverwriteHeader.astro
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
let gotError = false;
|
||||
try {
|
||||
Astro.response.headers = new Headers();
|
||||
} catch(err) {
|
||||
gotError = true;
|
||||
}
|
||||
---
|
||||
<div id="overwrite-error">{gotError}</div>
|
3
packages/astro/test/fixtures/ssr-response/src/components/SetAHeader.astro
vendored
Normal file
3
packages/astro/test/fixtures/ssr-response/src/components/SetAHeader.astro
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
Astro.response.headers.set('Seven-Eight', 'nine');
|
||||
---
|
3
packages/astro/test/fixtures/ssr-response/src/components/SetStatus.astro
vendored
Normal file
3
packages/astro/test/fixtures/ssr-response/src/components/SetStatus.astro
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
Astro.response.status = 403;
|
||||
---
|
12
packages/astro/test/fixtures/ssr-response/src/pages/child-set-status.astro
vendored
Normal file
12
packages/astro/test/fixtures/ssr-response/src/pages/child-set-status.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import SetStatus from '../components/SetStatus.astro';
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
<SetStatus />
|
||||
</body>
|
||||
</html>
|
14
packages/astro/test/fixtures/ssr-response/src/pages/child-tries-to-overwrite.astro
vendored
Normal file
14
packages/astro/test/fixtures/ssr-response/src/pages/child-tries-to-overwrite.astro
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import SetAHeader from '../components/SetAHeader.astro';
|
||||
import OverwriteHeader from '../components/OverwriteHeader.astro';
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
<SetAHeader />
|
||||
<OverwriteHeader />
|
||||
</body>
|
||||
</html>
|
14
packages/astro/test/fixtures/ssr-response/src/pages/some-header.astro
vendored
Normal file
14
packages/astro/test/fixtures/ssr-response/src/pages/some-header.astro
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import SetAHeader from '../components/SetAHeader.astro';
|
||||
Astro.response.headers.set('One-Two', 'three');
|
||||
Astro.response.headers.set('Four-Five', 'six');
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
<SetAHeader />
|
||||
</body>
|
||||
</html>
|
12
packages/astro/test/fixtures/ssr-response/src/pages/status-code.astro
vendored
Normal file
12
packages/astro/test/fixtures/ssr-response/src/pages/status-code.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
Astro.response.status = 404;
|
||||
Astro.response.statusText = 'Oops';
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -3,7 +3,6 @@ import { load as cheerioLoad } from 'cheerio';
|
|||
import { loadFixture } from './test-utils.js';
|
||||
import { viteID } from '../dist/core/util.js';
|
||||
|
||||
// Asset bundling
|
||||
describe('Integration buildConfig hook', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
|
|
@ -2,7 +2,6 @@ import { expect } from 'chai';
|
|||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
|
||||
// Asset bundling
|
||||
describe('API routes in SSR', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
|
|
@ -3,7 +3,6 @@ import { load as cheerioLoad } from 'cheerio';
|
|||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
|
||||
// Asset bundling
|
||||
describe('Dynamic pages in SSR', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
|
|
@ -3,7 +3,6 @@ import { load as cheerioLoad } from 'cheerio';
|
|||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
|
||||
// Asset bundling
|
||||
describe('Using Astro.request in SSR', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
|
62
packages/astro/test/ssr-response.test.js
Normal file
62
packages/astro/test/ssr-response.test.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { expect } from 'chai';
|
||||
import { load as cheerioLoad } from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
|
||||
describe('Using Astro.response in SSR', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/ssr-response/',
|
||||
adapter: testAdapter(),
|
||||
experimental: {
|
||||
ssr: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Can set the status', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/status-code');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it('Can set the statusText', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/status-code');
|
||||
const response = await app.render(request);
|
||||
expect(response.statusText).to.equal('Oops');
|
||||
});
|
||||
|
||||
it('Child component can set status', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/child-set-status');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(403);
|
||||
});
|
||||
|
||||
it('Can add headers', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/some-header');
|
||||
const response = await app.render(request);
|
||||
const headers = response.headers;
|
||||
expect(headers.get('one-two')).to.equal('three');
|
||||
expect(headers.get('four-five')).to.equal('six');
|
||||
expect(headers.get('seven-eight')).to.equal('nine');
|
||||
});
|
||||
|
||||
it('Child component cannot override headers object', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/child-tries-to-overwrite');
|
||||
const response = await app.render(request);
|
||||
const headers = response.headers;
|
||||
expect(headers.get('seven-eight')).to.equal('nine');
|
||||
const html = await response.text();
|
||||
const $ = cheerioLoad(html);
|
||||
expect($('#overwrite-error').html()).to.equal('true');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue