Skip to content

Commit

Permalink
Remove encryption of empty props to allow server island cacheability (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
kaytwo authored Jan 13, 2025
1 parent c7642fb commit 3aff68a
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-toes-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Removes encryption of empty props to allow server island cacheability
2 changes: 1 addition & 1 deletion packages/astro/src/core/server-islands/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/runtime/server/render/server-islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
import ComponentWithProps from '../components/ComponentWithProps.astro';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<ComponentWithProps server:defer greeting="Hello" />
</body>
</html>
65 changes: 65 additions & 0 deletions packages/astro/test/server-islands.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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',
);
});
});
});

Expand Down

0 comments on commit 3aff68a

Please sign in to comment.