diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 8d195bab4..c1adc7baf 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -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 = `` + 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); diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 27425aee5..55be7ba49 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -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 /** diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 598ec116f..2fad5d9c1 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -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'], }; diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index e9e74f9fa..debdbdbe0 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -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()); diff --git a/packages/astro/test/ssr-redirect.test.js b/packages/astro/test/ssr-redirect.test.js index bb4f747cd..922963e79 100644 --- a/packages/astro/test/ssr-redirect.test.js +++ b/packages/astro/test/ssr-redirect.test.js @@ -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'); + }); }); });