Headers for the URLs of a job
A publisher or converter job fetches files from URLs you give it and uploads the result to another one. When those URLs need a header, for example an Authorization token for a private CDN, put it in url_headers. This is one list of rules at the top level of the job, and every request the job makes to a URL a rule covers carries that rule's headers.
{
"url_headers": [
{
"match": "https://cdn.example.com/private/",
"headers": { "Authorization": "Bearer your-cdn-token" }
},
{
"match": "https://fonts.example.com/",
"headers": { "X-Api-Key": "your-font-key" }
}
],
"input": {
"bundle": { "url": "https://cdn.example.com/private/orders/995/bundle.json" }
},
"output": {
"upload": {
"url": "https://pod.example.com/webhooks/typograph/995",
"headers": { "Authorization": "Basic dXNlcjpwYXNz" }
},
"config": { "type": "pdf" }
}
}
Here the bundle download gets the CDN token, every font under https://fonts.example.com/ gets the font key, and the upload gets its own Authorization header.
The shape
url_headers is optional. It is a list of at most 10 rules, and each rule has exactly two fields:
| Field | Type | Description |
|---|---|---|
match | string | The https URL, or URL prefix, the headers are sent to |
headers | object | Header names and values, at most 5 per rule |
The per-URL headers you already know (input.template.headers, input.source.headers, output.upload.headers, output.config.watermark.image.headers and so on) keep working. They behave like an exact rule for that one URL, with one difference: an empty per-URL headers object is the same as leaving it out, so the prefix rules still apply. A per-URL headers object cannot opt a URL out; only a rule in url_headers with an empty headers object can (see below).
Which requests a rule covers
Every outgoing request of the job:
- Publisher: the downloads of
template,manifest,bundleandcsv, the check atPOST /jobsthat those URLs answer, the fonts and images the manifest names, the watermark image, and the upload. Withinput.csv, every child job the batch creates gets its own copy of the parent's rules for the requests it makes. - Converter: the download of
input.sourceand the upload.
Prefix or exact
- A
matchthat ends in/is a prefix. It covers every URL whose path starts with it:https://cdn.example.com/private/covershttps://cdn.example.com/private/a.jsonandhttps://cdn.example.com/private/fonts/b.woff2, whatever their query string. - Any other
matchis exact. It covers that one URL, query string included.
Which rule wins
For each request the headers come from one place only, in this order:
- the URL's own
headers(the per-URL shorthand); - otherwise an exact rule for that URL;
- otherwise the longest prefix that covers the URL;
- otherwise no headers.
A URL's own headers replace an exact rule in url_headers with the same match. That is not an error: the duplicate check below compares the rules in url_headers with each other only.
Headers of different rules are never merged. A URL covered by both https://cdn.example.com/ and https://cdn.example.com/private/ gets only the headers of the second.
Scheme, host and port are compared exactly
A rule only covers URLs with the same scheme, host and port: https://example.com/ does not cover https://cdn.example.com/ or https://example.com:8443/. There is no wildcard for subdomains. Give each host its own rule. Host names and schemes are compared case-insensitively, and the default port counts as given (https://example.com/ equals https://example.com:443/). Paths are compared case-sensitively, after . and .. segments are resolved.
Rules for a valid url_headers
A job that breaks one of these is refused with 422 Unprocessable Entity and "error": "validation_error":
- https only.
matchis an absolutehttpsURL with a host.httpis refused, and so are user info (user:pass@) and a fragment (#…). A prefix match cannot carry a query string. - No duplicates: two rules cannot have the same
match. - Limits: at most 10 rules, a
matchof at most 2000 characters (a presigned URL in an exact rule counts its query string), at most 5 headers per rule, and a header value of at most 8 KB (8192 bytes). A rule may have an emptyheadersobject: the URLs that rule wins for get no headers at all. On an exact rule that opts one URL out; on a prefix rule it opts out every URL below it, unless a longer prefix or an exact rule covers that URL. - No line breaks: a header value cannot contain CR, LF or NUL, and a header name must be a valid HTTP token.
- Only allowed header names, compared case-insensitively:
| Allowed | Names |
|---|---|
| Authentication | Authorization, X-Api-Key, X-Auth-Token, Api-Key |
| Content negotiation | Accept, Accept-Encoding, Accept-Language, Content-Type, Content-Disposition |
| Request tracking | X-Request-Id, X-Correlation-Id, X-Trace-Id |
| Caching | Cache-Control, If-None-Match, If-Modified-Since, ETag |
| Custom | any other X-… name |
Host, Cookie, Origin, Referer, the Proxy-… and connection headers, Content-Length, and X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, X-Real-Ip and X-Powered-By are always refused. On an upload, Content-Type and User-Agent are set by Typograph and cannot be overridden.
Presigned URLs and Authorization don't mix
S3, R2 and other object stores refuse a request that carries both a signature in the query string and an Authorization header ("only one auth mechanism allowed"). A broad prefix such as https://bucket.s3.example.com/ with an Authorization header therefore breaks every presigned URL below it.
- Keep prefixes narrow: cover the directory that needs the header, not the whole bucket.
- Where one URL below a prefix must go without the header, add an exact rule for it with an empty
headersobject. It wins over the prefix, so that URL gets no headers at all:
"url_headers": [
{ "match": "https://bucket.s3.example.com/", "headers": { "Authorization": "Bearer your-token" } },
{ "match": "https://bucket.s3.example.com/exports/995.pdf?X-Amz-Signature=abc", "headers": {} }
]
An exact rule matches the query string too, so give the presigned URL exactly as the job uses it. A URL's own non-empty headers also win over the prefix, and they replace its headers rather than adding to them.
What happens to the headers
- Never returned. No response, not
POST /jobsand notGET /jobs/{id}, containsurl_headersor anyheadersvalue, and neither does any webhook event. - Forgotten when the job ends. Typograph keeps the rules only while the job runs and deletes them once it is
completedorfailed. In a CSV batch each child job keeps its own copy until that child ends, so the parent finishing first does not take the rules away from its children. - A header is sent only to URLs its rule covers. A prefix rule sends the secret to every URL below that prefix, so keep prefixes to hosts and paths you control.
- Redirects are not followed. A download or upload that answers with a redirect fails the job, so a rule's headers never travel on to another host.
Limits of what headers reach
- Images inside an SVG. When an SVG image in the manifest refers to another image with
<image href="…">, that nested image is fetched without headers. Make it public, or give it a presigned URL. - The editor and canvas in the browser can't use them.
url_headersapplies to the server-side requests of a job only. The editor, canvas and viewer load fonts and images straight from the browser, so an asset that needs a header does not show there. For a template that is edited as well as published, use URLs the browser can load, such as presigned URLs.