Skip to content

Conversation

@Johannes-Andersen
Copy link

Changes

This is my first ever change to astro. So I am not sure this is the correct way. Therefore a more thorough review may be required.

The PR aims to implement the experimental CSP policy to the cloudflare adapter.

Mainly copied work from: de82ef2

Testing

Added tests. Existing tests should still pass.

Validate CF docs if output matches.

Example output:

/has-header
  cdn-cache-control: public, max-age=3600
  Content-Security-Policy: script-src 'self' 'sha256-BF0290pkb3jxQsE7z00xR8Imp8X34FLC88L0lkMnrGw=' 'sha256-QzWFZi+FLIx23tnm9SBU4aEgx4x8DsuASP07mfqol/c=' 'sha256-0chmwFk0zaA528yFfGV7J9ppIpdfTPPULncDF3WG7Zs=' 'sha256-eIXWvAmxkr251LJZkjniEK5LcPF3NkapbJepohwYRIc=' 'sha256-Q2BPg90ZMplYY+FSdApNErhpWafg2hcRRbndmvxuL/Q=' 'sha256-0oe0j1+KVmVYcHm1N1/3tGTf3Yhpnd6heIyJsO4LZS0=' 'sha256-Yfb49Tdgb3WWkhZG+Levy7SSbIwvHj9bEqvunWdMcuU='; style-src 'self' 'sha256-2sYicASgHviyvFSsZfd8xuNIhA0b8XasqB2neZePwjs=';

/parent/*/page
  Content-Security-Policy: script-src 'self' 'sha256-BF0290pkb3jxQsE7z00xR8Imp8X34FLC88L0lkMnrGw=' 'sha256-QzWFZi+FLIx23tnm9SBU4aEgx4x8DsuASP07mfqol/c=' 'sha256-0chmwFk0zaA528yFfGV7J9ppIpdfTPPULncDF3WG7Zs=' 'sha256-eIXWvAmxkr251LJZkjniEK5LcPF3NkapbJepohwYRIc=' 'sha256-Q2BPg90ZMplYY+FSdApNErhpWafg2hcRRbndmvxuL/Q=' 'sha256-0oe0j1+KVmVYcHm1N1/3tGTf3Yhpnd6heIyJsO4LZS0=' 'sha256-ByjyHeCbp9JwIvEz5gjdoP7siW4JyclCitgVszsv3/A='; style-src 'self' 'sha256-2sYicASgHviyvFSsZfd8xuNIhA0b8XasqB2neZePwjs=';

/
  Content-Security-Policy: script-src 'self' 'sha256-BF0290pkb3jxQsE7z00xR8Imp8X34FLC88L0lkMnrGw=' 'sha256-QzWFZi+FLIx23tnm9SBU4aEgx4x8DsuASP07mfqol/c=' 'sha256-0chmwFk0zaA528yFfGV7J9ppIpdfTPPULncDF3WG7Zs=' 'sha256-eIXWvAmxkr251LJZkjniEK5LcPF3NkapbJepohwYRIc=' 'sha256-Q2BPg90ZMplYY+FSdApNErhpWafg2hcRRbndmvxuL/Q=' 'sha256-0oe0j1+KVmVYcHm1N1/3tGTf3Yhpnd6heIyJsO4LZS0=' 'sha256-v10nX5cSfrpnBaKOp54plCw0M1DSmiPyeQyg//eGOnA='; style-src 'self' 'sha256-2sYicASgHviyvFSsZfd8xuNIhA0b8XasqB2neZePwjs=';

/*
  Content-Security-Policy: script-src 'self' 'sha256-BF0290pkb3jxQsE7z00xR8Imp8X34FLC88L0lkMnrGw=' 'sha256-QzWFZi+FLIx23tnm9SBU4aEgx4x8DsuASP07mfqol/c=' 'sha256-0chmwFk0zaA528yFfGV7J9ppIpdfTPPULncDF3WG7Zs=' 'sha256-eIXWvAmxkr251LJZkjniEK5LcPF3NkapbJepohwYRIc=' 'sha256-Q2BPg90ZMplYY+FSdApNErhpWafg2hcRRbndmvxuL/Q=' 'sha256-0oe0j1+KVmVYcHm1N1/3tGTf3Yhpnd6heIyJsO4LZS0=' 'sha256-84Lsggi8q1VFTLi2blghehwqNiFOYysZSnNhkaidDMk='; style-src 'self' 'sha256-2sYicASgHviyvFSsZfd8xuNIhA0b8XasqB2neZePwjs=';

Docs

Docs needs to be added, todo based on feedback.

@changeset-bot
Copy link

changeset-bot bot commented Jun 17, 2025

🦋 Changeset detected

Latest commit: a9ba349

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added the pkg: integration Related to any renderer integration (scope) label Jun 17, 2025
Copy link
Contributor

@ascorbic ascorbic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. I'm going to ask @ematipico for a review as he's the one who's working on this feature, but it looks good from my perspective.

@ascorbic ascorbic requested a review from ematipico June 18, 2025 08:25
Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Johannes-Andersen for your first contribution! I left some comments. I spotted some bugs, and asked a few questions.

One important thing that we have to address is to add use cases. Let me know if there's something that isn't clear.

Comment on lines 32 to 41
for (const line of raw.split(/\r?\n/)) {
if (!line.trim()) continue;
if (!/^\s/.test(line)) {
currentPattern = line.trim();
headersByPattern.set(currentPattern, headersByPattern.get(currentPattern) || {});
} else {
const [name, ...rest] = line.trim().split(':');
headersByPattern.get(currentPattern)![name.trim()] = rest.join(':').trim();
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first time that this adapter is in charge of _headers, which means that Astro owns its contents every time it is generated. In fact, its relative is loaded from outDir, instead of publicDir.

If the assumption is correct, it means that we don't need to read its contents, because they are generated anew at every build.

EDIT: I see that the tests use the public directory, which means that the code needs to be updated to use publicDir configuration. Also, I am fine to have this behaviour different from Netlify, at the end, it's up to the adapter how to use the feature. We need to make sure to:

  • update the documentation of the adapter, and explain that when experimentalStaticHeaders is enabled, Astro will update public/_headers
  • add more tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, right now a user could create a _headers file in public and it would work. We want to be able to not break these sites, so it makes sense to update the file in outDir (merging the user headers with the added ones) and leave the one in public untouched.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense!

const csp = heads.get('Content-Security-Policy');
if (!csp) continue;

const pattern = segmentsToCfSyntax(route.segments, config);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, is there a reason why you didn't use createHostRouteDefinition like we do in Netlify?

const definition = createHostedRouteDefinition(route, config);

The logic behind it has already been used for the _redirects file in Cloudflare, which means that it's battle-tested, and it provides all the information to create the correct Unix-like paths.

Comment on lines 1 to 2
/has-header
cdn-cache-control: public, max-age=3600
Copy link
Member

@ematipico ematipico Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the adapter reads this file and updates it, we need to ensure that we join routes correctly. Which means that if this file specifies a route e.g. /server-island with some headers, and Astro emits the CSP header for this very route, the final file should contain both headers.

Can we add few more tests that cover more Astro routes:

  • static routes e.g. src/pages/server-island.astro
  • dynamic routes e.g. src/pages/posts/[slug].astro
  • catch all route e.g. src/pages/blog/[...catchAll].astro

And have few routes in _headers that match some of those routes and make sure they all have all headers merged.

/server-island
  cdn-cache-control: public, max-age=3600
/posts/intro
  cdn-cache-control: public, max-age=3600
/blog/hello-word
  cdn-cache-control: public, max-age=3600

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I would add some more built-in routes to the tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexanderniebuhr I tried adding more. Would love to hear what more routes you would like tested :D

@Johannes-Andersen
Copy link
Author

Thank you @Johannes-Andersen for your first contribution! I left some comments. I spotted some bugs, and asked a few questions.

One important thing that we have to address is to add use cases. Let me know if there's something that isn't clear.

Thanks for the review! The feedback you left makes sense! I'll have a go at fixing them tomorrow. A bit busy today ☺️

@Johannes-Andersen Johannes-Andersen force-pushed the feat/cloudflareStaticHeaders branch from c5a5ff8 to 0667f93 Compare June 19, 2025 21:45
Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for addressing my comments! I tried to merge main again, because some tests were failing and I don't know why

@ematipico
Copy link
Member

@Johannes-Andersen we are in the process of changing the payload, so we're holding off merging the PR. However, in the meantime, can you please address the CI failures?

@ematipico
Copy link
Member

@Johannes-Andersen heads-up that this PR #13972 is going to change how we generate the static headers for the page

@alexanderniebuhr
Copy link
Member

I will also review this tomorrow :)

@alexanderniebuhr alexanderniebuhr self-requested a review July 1, 2025 18:36
Comment on lines +36 to +41
const output =
[...headersByPattern]
.map(([pattern, headers]) =>
[pattern, ...Object.entries(headers).map(([k, v]) => ` ${k}: ${v}`)].join('\n'),
)
.join('\n\n') + '\n';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have a shared util from astro for this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be aware that this PR #13972 will change headersByPattern, so we will probably have to update this code.

Nonetheless, we have @astrojs/underscore-redirects that might provide something we can use here, but you should look into it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was thinking about printAsRedirects from @astrojs/underscore-redirects, but I'm not sure if the syntax is 100% equal

Comment on lines +50 to +72
async function loadExistingHeaders(publicUrl: URL): Promise<Map<string, Record<string, string>>> {
try {
const text = await readFile(publicUrl, 'utf-8');
return text
.split(/\r?\n/)
.filter(Boolean)
.reduce(
(map, line) => {
if (!line.startsWith(' ')) {
map.current = line.trim();
map.store.set(map.current, map.store.get(map.current) ?? {});
} else {
const [key, ...rest] = line.trim().split(':');
map.store.get(map.current)![key.trim()] = rest.join(':').trim();
}
return map;
},
{ current: '', store: new Map<string, Record<string, string>>() },
).store;
} catch {
return new Map();
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good for now, but in the long term I think we should also have this shared from astro, I feel like this could also apply to other providers not just Cloudflare. WDYT @ematipico?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we can. Each adapter (i.e., hosting platform) has its own configuration file, with its own syntax. For example, with Netlify we use their config.json file. With the Node.js adapter, we created a custom JSON file with a different structure

Copy link
Member

@alexanderniebuhr alexanderniebuhr Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I thought the _headers file is a common syntax, than disregard my comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the _headers file is a kinda-common syntax. I think it's generally supported in the same places that support _redirects, so there is a good argument for adding support in that package. We don't use it on Netlify now, as we use their new framework API. On Netlify at least, the _headers file is now mainly created by users rather than auto-generated by frameworks.

Copy link
Member

@sarah11918 sarah11918 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just fixing a quick changeset nit!

@sarah11918
Copy link
Member

sarah11918 commented Jul 2, 2025

For docs, noting that an accompanying docs PR should add experimentalStaticHeaders as an option in this section of the Cloudflare adapter guide (https://docs.astro.build/en/guides/integrations-guide/cloudflare/#options)

(By the time this merges, there should already be something similar written for the Node, Netlify, and Vercel adapters, so you can basically copy from them.)

@florian-lefebvre
Copy link
Member

@Johannes-Andersen Hi! Are you still interested in this PR?

@Johannes-Andersen
Copy link
Author

@Johannes-Andersen Hi! Are you still interested in this PR?

Got a bit much happening in life and at work at the moment 😅
So while I still would like to finish this up, I currently do not have the capacity

@florian-lefebvre
Copy link
Member

No problem, thanks for opening the PR in the first place!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: integration Related to any renderer integration (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants