0
Fork 0
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:
Matthew Phillips 2022-05-05 12:21:53 -04:00 committed by GitHub
parent d0a4064d80
commit 61e1a267a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 177 additions and 15 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Implements the Astro.response RFC

View file

@ -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;
}

View file

@ -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(

View file

@ -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
};
}

View file

@ -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,
};
}

View file

@ -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;

View file

@ -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(

View file

@ -0,0 +1,9 @@
---
let gotError = false;
try {
Astro.response.headers = new Headers();
} catch(err) {
gotError = true;
}
---
<div id="overwrite-error">{gotError}</div>

View file

@ -0,0 +1,3 @@
---
Astro.response.headers.set('Seven-Eight', 'nine');
---

View file

@ -0,0 +1,3 @@
---
Astro.response.status = 403;
---

View file

@ -0,0 +1,12 @@
---
import SetStatus from '../components/SetStatus.astro';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<SetStatus />
</body>
</html>

View 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>

View 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>

View 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>

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View 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');
});
});