Redirects spike

This commit is contained in:
Matthew Phillips 2023-05-10 14:47:14 -04:00
parent 48395c8152
commit 46e726960f
5 changed files with 102 additions and 47 deletions

View file

@ -33,7 +33,7 @@ import {
createAPIContext,
throwIfRedirectNotAllowed,
} from '../endpoint/index.js';
import { AstroError } from '../errors/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { debug, info } from '../logger/core.js';
import { callMiddleware } from '../middleware/callMiddleware.js';
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
@ -72,6 +72,12 @@ export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings):
return facadeId.slice(fileURLToPath(settings.config.root).length);
}
function redirectWithNoLocation() {
throw new AstroError({
...AstroErrorData.RedirectWithNoLocation
});
}
// Determines of a Rollup chunk is an entrypoint page.
export function chunkIsPage(
settings: AstroSettings,
@ -510,10 +516,23 @@ async function generatePath(
}
throw err;
}
throwIfRedirectNotAllowed(response, opts.settings.config);
// If there's no body, do nothing
if (!response.body) return;
body = await response.text();
switch(response.status) {
case 301:
case 302: {
const location = response.headers.get("location");
if(!location) {
redirectWithNoLocation();
}
body = `<!doctype html><meta http-equiv="refresh" content="0;url=${location}" />`
break;
}
default: {
// If there's no body, do nothing
if (!response.body) return;
body = await response.text();
}
}
}
const outFolder = getOutFolder(settings.config, pathname, pageData.route.type);

View file

@ -45,6 +45,7 @@ export const AstroErrorData = {
* To redirect on a static website, the [meta refresh attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) can be used. Certain hosts also provide config-based redirects (ex: [Netlify redirects](https://docs.netlify.com/routing/redirects/)).
*/
StaticRedirectNotAvailable: {
// TODO remove
title: '`Astro.redirect` is not available in static mode.',
code: 3001,
message:
@ -717,6 +718,18 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
return `The information stored in \`Astro.locals\` for the path "${href}" is not serializable.\nMake sure you store only serializable data.`;
},
},
/**
* @docs
* @see
* - [Astro.redirect](https://docs.astro.build/en/guides/server-side-rendering/#astroredirect)
* @description
* A redirect must be given a location with the `Location` header.
*/
RedirectWithNoLocation: {
// TODO remove
title: 'A redirect must be given a location with the `Location` header.',
code: 3035,
},
// No headings here, that way Vite errors are merged with Astro ones in the docs, which makes more sense to users.
// Vite Errors - 4xxx
/**

View file

@ -204,23 +204,21 @@ export function createResult(args: CreateResultArgs): SSRResult {
locals,
request,
url,
redirect: args.ssr
? (path, status) => {
// If the response is already sent, error as we cannot proceed with the redirect.
if ((request as any)[responseSentSymbol]) {
throw new AstroError({
...AstroErrorData.ResponseSentError,
});
}
redirect(path, status) {
// If the response is already sent, error as we cannot proceed with the redirect.
if ((request as any)[responseSentSymbol]) {
throw new AstroError({
...AstroErrorData.ResponseSentError,
});
}
return new Response(null, {
status: status || 302,
headers: {
Location: path,
},
});
}
: onlyAvailableInSSR('Astro.redirect'),
return new Response(null, {
status: status || 302,
headers: {
Location: path,
},
});
},
response: response as AstroGlobal['response'],
slots: astroSlots as unknown as AstroGlobal['slots'],
};

View file

@ -57,6 +57,12 @@ export function stringifyChunk(
}
return renderAllHeadContent(result);
}
default: {
if(chunk instanceof Response) {
return '';
}
throw new Error(`Unknown chunk type: ${(chunk as any).type}`);
}
}
} else {
if (isSlotString(chunk as string)) {
@ -102,6 +108,7 @@ export function chunkToByteArray(
if (chunk instanceof Uint8Array) {
return chunk as Uint8Array;
}
// stringify chunk might return a HTMLString
let stringified = stringifyChunk(result, chunk);
return encoder.encode(stringified.toString());

View file

@ -6,34 +6,52 @@ describe('Astro.redirect', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-redirect/',
output: 'server',
adapter: testAdapter(),
describe('output: "server"', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-redirect/',
output: 'server',
adapter: testAdapter(),
});
await fixture.build();
});
it('Returns a 302 status', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/secret');
const response = await app.render(request);
expect(response.status).to.equal(302);
expect(response.headers.get('location')).to.equal('/login');
});
it('Warns when used inside a component', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/late');
const response = await app.render(request);
try {
const text = await response.text();
expect(false).to.equal(true);
} catch (e) {
expect(e.message).to.equal(
'The response has already been sent to the browser and cannot be altered.'
);
}
});
await fixture.build();
});
it('Returns a 302 status', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/secret');
const response = await app.render(request);
expect(response.status).to.equal(302);
expect(response.headers.get('location')).to.equal('/login');
});
it('Warns when used inside a component', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/late');
const response = await app.render(request);
try {
const text = await response.text();
expect(false).to.equal(true);
} catch (e) {
expect(e.message).to.equal(
'The response has already been sent to the browser and cannot be altered.'
);
}
describe('output: "static"', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-redirect/',
output: 'static',
});
await fixture.build();
});
it.only('Includes the meta refresh tag.', async () => {
const html = await fixture.readFile('/secret/index.html');
expect(html).to.include('http-equiv="refresh');
expect(html).to.include('url=/login');
});
});
});