Content-Disposition is an HTTP response header that tells the browser whether to display the body in the window (inline) or save it as a file (attachment), and which filename to suggest.
It decides what happens when a user clicks a link to a screenshot or a PDF: does the image open in a tab, or does a file appear in Downloads? This entry is part of the screenshot glossary.
What it means in plain terms
The header has a type and optional parameters:
Content-Disposition: inline Content-Disposition: inline; filename="report.pdf" Content-Disposition: attachment; filename="report.pdf" Content-Disposition: attachment; filename="resume.png"; filename*=UTF-8''r%C3%A9sum%C3%A9.png
inline: show it if you can. Images, PDFs, video and HTML render in the tab. The filename is used if the user then saves the file.attachment: do not render it, save it. The browser starts a download.filename: the suggested name, plain ASCII in quotes.filename*: the same name in UTF-8, percent-encoded (RFC 6266 with RFC 8187 encoding). When both are present, modern browsers preferfilename*.
The header only says what to do with a body. The MIME type in Content-Type still says what the body is.
How the browser decides
flowchart TD A[Response arrives] --> B{Content-Disposition?} B -->|attachment| D["Download<br/>name from filename* or filename"] B -->|inline or missing| C{"Can the browser render<br/>this Content-Type?"} C -->|yes: image, PDF, HTML| E[Show in the tab] C -->|no| D F["a href download (same origin only)"] -.->|forces| D
The download attribute on a link can also force a download, but only for same-origin URLs. A link from your site to an image on an API domain or a CDN ignores it. That is why "add download to the link" often does nothing for screenshot URLs.
Testing it with Puppeteer, Playwright and RenderScreenshot
To see both behaviours, we served the same PNG from a small Node server, once as inline and once as attachment, with a UTF-8 filename:
// cd-server.mjs: serves the same PNG twice, once inline and once as an attachment. import { createServer } from 'node:http'; import { readFileSync } from 'node:fs'; const png = readFileSync(process.argv[2]); const name = 'capture-résumé.png'; // ASCII fallback in filename, UTF-8 name in filename*. const disposition = (type) => `${type}; filename="capture-resume.png"; filename*=UTF-8''${encodeURIComponent(name)}`; createServer((req, res) => { const type = req.url.startsWith('/download') ? 'attachment' : 'inline'; res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Disposition': disposition(type) }); res.end(png); }).listen(8077);
Playwright accepts downloads by default. Navigation to an attachment rejects page.goto with "Download is starting", so wait for the download event at the same time:
import { chromium } from 'playwright'; const browser = await chromium.launch(); try { const page = await browser.newPage(); const res = await page.goto('http://127.0.0.1:8077/view'); console.log('inline:', res.status()); const [download] = await Promise.all([ page.waitForEvent('download'), page.goto('http://127.0.0.1:8077/download').catch(() => {}), // rejects: "Download is starting" ]); console.log('attachment saved as:', download.suggestedFilename()); await download.saveAs(`./${download.suggestedFilename()}`); } finally { await browser.close(); }
It printed attachment saved as: capture-résumé.png: Chromium used filename*, not the ASCII fallback.
Puppeteer has no download event, so tell Chrome where to save through the DevTools Protocol:
import puppeteer from 'puppeteer'; import { mkdir, readdir } from 'node:fs/promises'; import { resolve } from 'node:path'; const dir = resolve('downloads'); await mkdir(dir, { recursive: true }); const browser = await puppeteer.launch(); try { const page = await browser.newPage(); const cdp = await browser.target().createCDPSession(); await cdp.send('Browser.setDownloadBehavior', { behavior: 'allow', downloadPath: dir, eventsEnabled: true }); const done = new Promise((r) => cdp.on('Browser.downloadProgress', (e) => e.state === 'completed' && r())); await page.goto('http://127.0.0.1:8077/download').catch((e) => console.log('goto:', e.message)); await done; console.log('files:', await readdir(dir)); } finally { await browser.close(); }
Output: goto: net::ERR_ABORTED at http://127.0.0.1:8077/download, then files: [ 'capture-résumé.png' ]. The ERR_ABORTED is expected: the navigation became a download.
RenderScreenshot answers a binary capture with inline, so a link to the image or PDF opens it in the tab:
HTTP/1.1 200 OK Content-Type: image/png Content-Disposition: inline; filename="screenshot.png" X-Cache-URL: https://cdn.renderscreenshot.com/...
A PDF arrives as inline; filename="screenshot.pdf". If your users need a "Download" button with a named file, serve the capture from your own route and set attachment there, for example in Rails:
# app/controllers/reports_controller.rb def download pdf = ScreenshotClient.pdf_for(report_url(params[:id])) # your wrapper around the API call send_data pdf, type: 'application/pdf', disposition: 'attachment', filename: "report-#{params[:id]}.pdf" end
Rails' send_data writes both filename and filename*. The full list of response headers is in response headers, and PDF output in PDF options.
Tested on 2026-10-15 on macOS 27 (arm64) with Node 22.18.0, [email protected] (Chrome 148.0.7778.97, headless: 'shell') and [email protected]. The Rails controller is a fragment (ScreenshotClient stands for your own code) and was not run.
Common mistakes
- Non-ASCII in
filename. Raw UTF-8 in the quotedfilenameis not valid and different browsers read it differently. Put an ASCII fallback infilenameand the real name infilename*. - Unescaped quotes or user input in the name. A name with
"or a newline can break the header. Build it from a safe slug, not raw user text. - Relying on
downloadacross origins. It is ignored for other origins. Set the header on the server, or proxy the file. - Expecting
attachmentto stop caching or access. The header controls display, not who can fetch the URL or for how long. Use cache headers and signed URLs for that.
Related terms
To render PDFs and images without running a browser, create a free account.