Skip to main content

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:

FieldTypeDescription
matchstringThe https URL, or URL prefix, the headers are sent to
headersobjectHeader 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, bundle and csv, the check at POST /jobs that those URLs answer, the fonts and images the manifest names, the watermark image, and the upload. With input.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.source and the upload.

Prefix or exact​

  • A match that ends in / is a prefix. It covers every URL whose path starts with it: https://cdn.example.com/private/ covers https://cdn.example.com/private/a.json and https://cdn.example.com/private/fonts/b.woff2, whatever their query string.
  • Any other match is 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:

  1. the URL's own headers (the per-URL shorthand);
  2. otherwise an exact rule for that URL;
  3. otherwise the longest prefix that covers the URL;
  4. 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. match is an absolute https URL with a host. http is 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 match of 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 empty headers object: 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:
AllowedNames
AuthenticationAuthorization, X-Api-Key, X-Auth-Token, Api-Key
Content negotiationAccept, Accept-Encoding, Accept-Language, Content-Type, Content-Disposition
Request trackingX-Request-Id, X-Correlation-Id, X-Trace-Id
CachingCache-Control, If-None-Match, If-Modified-Since, ETag
Customany 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 headers object. 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 /jobs and not GET /jobs/{id}, contains url_headers or any headers value, 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 completed or failed. 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_headers applies 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.