Working with response headers in on-demand builder pages in Astro

Hey team, hope y’all are doing well!

I’m working on an Astro site here:

And I’m using the Netlify adapter to make pages with On-Demand Builders. The pages are made via an API call that pings the page with markdown in the headers.

Here is an example Postman request.
Here’s an example page made with said request:

But, when I try to make this request from a web page, I get a CORS error:

Access to fetch at ' from origin ‘http://localhost:5173’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

I tried adding the headers on the share-pear side, both in the Astro configuration:

import { defineConfig } from "astro/config";
import netlify from "@astrojs/netlify/functions";

export default defineConfig({
	output: "server",
	server: {
		headers: {
			"Access-Control-Allow-Origin": "*"
	adapter: netlify({
		builders: true

And also in the netlify.toml:

  for = "/*"
	Access-Control-Allow-Origin = "*"

The headers appear when I run the site locally, but nothing is working with the deploy preview. Is there something I have to do to make this work with on-demand builders?

1 Like

Heya Cassidy, good to see you again!

I suppose there are a couple ways to approach it. One is what Scott described in slack: use a proxy route to access these paths, so the request comes from netlify instead of from localhost or some other host:

/share/* 200!

The other way to approach it - potentially via custom headers - is the one you seem to be trying. Note that only the config in your lambda is going to work if a lambda serves the path - custom headers in netlify.toml are explicitly not applied to proxy’d routes such as functions!

Can you request the function directly, and see your headers (from the lambda code) being returned?

Hey Fool!

To clarify, this is not working locally nor deployed, yet. Everything’s deployed on Netlify right now (both the querying site and the on-demand builders site).

The repo itself is here for the ODB site:

There’s not an actual lambda config or function with Astro, it’s a dynamic page route that pulls in header information here:

And this is just the configuration I have:

So the custom header stuff is what I’m trying both in the netlify.toml and also in that astro.config.js in this PR with no dice with either:

If I’m understanding correctly, is there no way to update ODB headers unless you use a proxy? Or maybe I wasn’t clear?

The headers do show up properly locally with my dev server, but don’t show up once deployed, so that’s where I’m wondering if it’s a Netlify-specific configuration I need to set up.

This might be more of a question for the Astro team but I’m just grasping for anything at this point: I was also looking at the documentation for Astro and Edge functions, and it sounds like id I want to use both Edge functions and on-demand builders, the blog here tells me to use edge-functions for Edge functions, but the docs linked here say that I need to use just functions for ODB. Do I use both? Or just edge-functions? Is that a limitation on the Astro side, or the Netlify side?

Thank you again for responding!!

you should be able to set custom headers in your s ource code somehow. In next.js, you’d put them in next.js.config and anything we “turn into” a function will use them. I don’t know enough about how Astro works under the covers to say:

  • if we turn something like this into a function (or whether it is being turned into an Edge Function, or Lambda in this case)
  • if we do, what the appropriate config for an Astro function should be, to cause that same behavior.

I understand it’s not working yet, but if you can tell me the URL that is failing, I can at least tell you if it is being served by a static file or a lambda, so you can try to figure out the right place to put the right config?

So what I’m trying to do is make it so that if you hit one of the pages under share/*, like with a GET request, if that GET request has a markdown property in the header, it generates a page with On-Demand Builders. If it doesn’t, then it just says “no content found”.

That all works if I call it from Postman or something. But, because I’m calling it from another website, I need to set the headers to allow it to make the GET requests, and I’m dealing with the preflight nonsense that browsers have there.
From what I’m gathering, I think that I can’t set the headers for On-Demand Builders without a middleware or proxy of some kind? I was just reading the docs to see if I missed something and I’m wondering if the line “They don’t provide access to HTTP headers or query parameters from incoming requests.” means that. My latest attempt is using Edge Functions to handle that.

So anyway, that URL (or any other URL after /share/) will automatically give you the “no content found” when you test it, unless you make the request with that Postman function that I put in the original post.

If there’s somewhere else to set headers, I’m all ears, but yeah, the netlify.toml and the astro.config.js route hasn’t worked yet.

I figured out the solution and I’m working on a blog about it! I ended up writing an edge function that set all of the headers, including dealing with preflight.

// /netlify/edge-functions/headers.js
export default async (request, context) => {
	const response = await;

	// We need to include this OPTIONS request to handle CORS
	// from requests inside the browser, because preflight
	if (request.method === "OPTIONS") {
		return new Response("ok", {
			headers: {
				"Access-Control-Allow-Origin": "*",
				"Access-Control-Allow-Headers": "Content-Type, markdown",

	// This currently allows requests from anywhere, but we'll\
	// restrict it to individual domains we own
	response.headers.set("Access-Control-Allow-Origin", "*");

	// This allows our custom markdown header that Astro consumes
		"Content-Type, markdown"

	// This sets a max-age for the cache of 30 days
	response.headers.set("Cache-Control", "public, max-age=2592000, immutable");

	return response;

The blog will be live on the Contenda blog soon!

Thanks again team for helping!