Remove encryption of empty props to allow server island cacheability (#12956)

* tests for cacheable server islands with no props

* changeset

* allow server islands to omit encrypted props when they do not have any props

* prod and dev tests
This commit is contained in:
Chris Kanich 2025-01-13 03:24:30 -08:00 committed by GitHub
parent c7642fb80b
commit 3aff68a419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 95 additions and 2 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Removes encryption of empty props to allow server island cacheability

View file

@ -120,7 +120,7 @@ export function createEndpoint(manifest: SSRManifest) {
const key = await manifest.key;
const encryptedProps = data.encryptedProps;
const propString = await decryptString(key, encryptedProps);
const propString = encryptedProps === '' ? '{}' : await decryptString(key, encryptedProps);
const props = JSON.parse(propString);
const componentModule = await imp();

View file

@ -76,7 +76,8 @@ export function renderServerIsland(
}
const key = await result.key;
const propsEncrypted = await encryptString(key, JSON.stringify(props));
const propsEncrypted =
Object.keys(props).length === 0 ? '' : await encryptString(key, JSON.stringify(props));
const hostId = crypto.randomUUID();

View file

@ -0,0 +1,11 @@
---
await new Promise(resolve => setTimeout(resolve, 1));
Astro.response.headers.set('X-Works', 'true');
export type Props = {
greeting?: string;
};
const greeting = Astro.props?.greeting ? Astro.props.greeting : 'default greeting';
---
<div id="islandContent">
<div id="greeting">{greeting}</div>
</div>

View file

@ -0,0 +1,11 @@
---
import ComponentWithProps from '../components/ComponentWithProps.astro';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<ComponentWithProps server:defer greeting="Hello" />
</body>
</html>

View file

@ -61,6 +61,36 @@ describe('Server islands', () => {
const works = res.headers.get('X-Works');
assert.equal(works, 'true', 'able to set header from server island');
});
it('omits empty props from the query string', async () => {
const res = await fixture.fetch('/');
assert.equal(res.status, 200);
const html = await res.text();
const fetchMatch = html.match(/fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/s);
assert.equal(fetchMatch.length, 2, 'should include props in the query string');
assert.equal(fetchMatch[1], '', 'should not include encrypted empty props');
});
it('re-encrypts props on each request', async () => {
const res = await fixture.fetch('/includeComponentWithProps/');
assert.equal(res.status, 200);
const html = await res.text();
const fetchMatch = html.match(
/fetch\('\/_server-islands\/ComponentWithProps\?[^']*p=([^&']*)/s,
);
assert.equal(fetchMatch.length, 2, 'should include props in the query string');
const firstProps = fetchMatch[1];
const secondRes = await fixture.fetch('/includeComponentWithProps/');
assert.equal(secondRes.status, 200);
const secondHtml = await secondRes.text();
const secondFetchMatch = secondHtml.match(
/fetch\('\/_server-islands\/ComponentWithProps\?[^']*p=([^&']*)/s,
);
assert.equal(secondFetchMatch.length, 2, 'should include props in the query string');
assert.notEqual(
secondFetchMatch[1],
firstProps,
'should re-encrypt props on each request with a different IV',
);
});
});
describe('prod', () => {
@ -103,6 +133,41 @@ describe('Server islands', () => {
const response = await app.render(request);
assert.equal(response.headers.get('x-robots-tag'), 'noindex');
});
it('omits empty props from the query string', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
assert.equal(response.status, 200);
const html = await response.text();
const fetchMatch = html.match(/fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/s);
assert.equal(fetchMatch.length, 2, 'should include props in the query string');
assert.equal(fetchMatch[1], '', 'should not include encrypted empty props');
});
it('re-encrypts props on each request', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/includeComponentWithProps/');
const response = await app.render(request);
assert.equal(response.status, 200);
const html = await response.text();
const fetchMatch = html.match(
/fetch\('\/_server-islands\/ComponentWithProps\?[^']*p=([^&']*)/s,
);
assert.equal(fetchMatch.length, 2, 'should include props in the query string');
const firstProps = fetchMatch[1];
const secondRequest = new Request('http://example.com/includeComponentWithProps/');
const secondResponse = await app.render(secondRequest);
assert.equal(secondResponse.status, 200);
const secondHtml = await secondResponse.text();
const secondFetchMatch = secondHtml.match(
/fetch\('\/_server-islands\/ComponentWithProps\?[^']*p=([^&']*)/s,
);
assert.equal(secondFetchMatch.length, 2, 'should include props in the query string');
assert.notEqual(
secondFetchMatch[1],
firstProps,
'should re-encrypt props on each request with a different IV',
);
});
});
});