Read a PDF as bytes (or as a stream) and return those bytes as the response body. Set Content-Type: application/pdf. Use Content-Disposition: inline when the browser should try to display the document, or attachment; filename="report.pdf" when it should download it. The implementation depends on your framework and on whether the PDF is an in-memory buffer, a trusted file, or a stream.
Choose the response strategy first
There are three practical ways to send a PDF:
| Source | Best response approach | Memory and safety considerations |
|---|---|---|
| Bytes already in memory | Wrap the bytes in a binary file-like object or framework buffer response. | Convenient for appropriately sized documents; the whole PDF is already in application memory. |
| Trusted server-side file | Use the framework’s file-serving API. | Often lets the framework manage metadata and transfer behavior. Never turn an unrestricted request parameter into a path. |
| Large or incrementally generated PDF | Use a stream-capable response. | Can avoid buffering the entire document, but failures after transmission starts may produce only a partial response. |
Before sending anything, make sure the PDF was generated or retrieved successfully. If generation fails before headers are sent, return a normal error response. Once response headers or body bytes have reached the client, many servers cannot replace the partial PDF with a JSON error; adapter behavior differs, so handle stream errors explicitly.
Set the headers that control browser behavior
Content type
Set Content-Type to application/pdf. This tells the client what the response body represents. Framework helpers may call the setting mimetype, contentType, or an equivalent option.
Inline preview or download
Content-Disposition: inline expresses that the browser may display the PDF in its built-in viewer. Content-Disposition: attachment; filename="report.pdf" expresses that it should offer a download with the suggested filename. The final behavior can still depend on browser settings and extensions.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
#1 Best Overall
Filename hygiene
Use a fixed or validated filename. Do not copy arbitrary path text or an unsanitized user value into a filesystem path or response header. If a user chooses a document, map an identifier to a server-side record or constrain file resolution to a known root directory.
Flask: return bytes, a file, or a stream
In-memory PDF bytes
Flask’s send_file accepts a filesystem path or a file-like object. For in-memory data, use a binary-mode object and rewind it to position zero before returning it.
from io import BytesIO
from flask import Flask, send_file
app = Flask(__name__)
@app.get("/report.pdf")
def report():
pdf_bytes = build_pdf_bytes() # Return valid PDF bytes from your generator.
pdf_file = BytesIO(pdf_bytes)
pdf_file.seek(0)
return send_file(
pdf_file,
mimetype="application/pdf",
as_attachment=False,
download_name="report.pdf",
)
def build_pdf_bytes():
# Replace this with your PDF generator or storage lookup.
return b"%PDF-1.4n..."
if __name__ == "__main__":
app.run()
as_attachment=False produces inline-oriented metadata. Set it to True for a download; retain download_name to suggest the filename. A file-like object must be opened in binary mode (for example, BytesIO), not text mode.
Trusted filesystem file
from flask import send_file
@app.get("/reports/<report_id>.pdf")
def stored_report(report_id):
path = lookup_trusted_report_path(report_id) # Database/allow-list lookup.
if path is None:
return {"error": "Not found"}, 404
return send_file(
path,
mimetype="application/pdf",
as_attachment=True,
download_name="report.pdf",
)
Flask’s documentation prefers paths in most cases, but the path must come from trusted server-side resolution. Do not pass a request-supplied path directly to send_file.
Express: send a trusted file or a generated buffer
Serve a file with res.download
import express from "express";
const app = express();
app.get("/reports/:id/download", (req, res, next) => {
const filePath = lookupTrustedReportPath(req.params.id);
if (!filePath) return res.sendStatus(404);
res.download(filePath, "report.pdf", { root: TRUSTED_REPORT_ROOT }, (err) => {
if (err && !res.headersSent) next(err);
});
});
app.listen(3000);
Express documents res.download(path, filename, options, callback). A root constraint can limit resolution, and the callback lets you distinguish an error that occurred before headers from one that occurred after transfer began. The example still performs an allow-listed lookup; a root option is not a reason to accept arbitrary path text.
Return a PDF buffer inline
app.get("/report.pdf", async (req, res, next) => {
try {
const pdfBuffer = await buildPdfBuffer();
res.type("application/pdf");
res.set("Content-Disposition", "inline; filename="report.pdf"");
res.send(pdfBuffer);
} catch (err) {
next(err);
}
});
Express’s res.send can transmit a buffer. Set the media type explicitly rather than relying on a filename or automatic inference.
NestJS: stream with StreamableFile
NestJS documents StreamableFile for returning a readable stream or buffer together with response metadata.
import { Controller, Get } from '@nestjs/common';
import { StreamableFile } from '@nestjs/common';
import { createReadStream } from 'node:fs';
@Controller('reports')
export class ReportsController {
@Get('latest.pdf')
getLatest(): StreamableFile {
const stream = createReadStream('/srv/reports/latest.pdf');
return new StreamableFile(stream, {
type: 'application/pdf',
disposition: 'inline; filename="latest.pdf"',
});
}
}
For a generated document, pass a readable stream or a buffer instead of createReadStream. NestJS describes adapter-specific stream error behavior for Express and Fastify. Attach appropriate error handling and decide what to do when the stream fails before versus after the response has started.
Framework-independent HTTP shape
Regardless of framework, the wire response has the same essential structure:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: inline; filename="report.pdf"
<raw PDF bytes>
Do not base64-encode the body unless a separate protocol requires it. A normal PDF download response carries the binary bytes directly. If the request is not authorized, the document does not exist, or generation failed before transmission, return the appropriate non-success status and an error representation instead of pretending the body is a PDF.
Secure file selection
- Map public IDs to records and server-controlled paths; do not concatenate a query parameter into a path.
- Keep downloadable files below an approved directory and use the framework’s root restriction where available.
- Check authorization before opening the file or starting the stream.
- Use a controlled filename in
Content-Disposition; avoid line breaks and path separators. - For private documents, apply the same authentication and authorization checks to every request, including cached or signed download URLs.
Performance and reliability decisions
When buffering is reasonable
In-memory bytes simplify generated reports and make it easy to set headers before sending. Use this when the document is appropriately sized for your process and concurrent requests will not exhaust memory.
When to prefer a path
A trusted path lets Flask, Express, or another framework manage file transfer without first copying the entire document into a new application buffer. It is a good fit when a PDF is already stored on disk.
Recommended Free Tools
When to stream
Stream when a PDF is large, generated incrementally, or coming from an upstream source. Plan for cancellation and stream errors. After the first bytes are sent, replacing the response with a clean JSON error is generally no longer possible; clients may receive a partial, unreadable PDF.
Verify what clients receive
Test both disposition modes with a real browser and an HTTP client. Confirm the status, Content-Type, Content-Disposition, filename, and that the body begins with valid PDF data. Test missing documents, unauthorized IDs, generator failures, client disconnects, and a file that is larger than normal.
Troubleshooting common failures
The browser downloads HTML or JSON instead of a PDF
Inspect the status code and headers. An authentication redirect, framework error page, or exception handler may be returning a non-PDF body. Ensure the success path sets application/pdf and that errors are handled before the PDF response starts.
The PDF viewer says the file is damaged
Check that the response body contains raw PDF bytes, that an in-memory stream is positioned at zero, and that no text encoding, debug output, or JSON wrapper was prepended. For streamed generation, investigate whether the stream failed partway through.
send_file raises a file or mode error
Pass a valid server-side path or a binary file-like object. Do not open an in-memory PDF in text mode, and do not pass an unchecked user path.
Express returns a path error or exposes files
Replace direct path construction with an ID-to-path lookup and constrain the download root. Reject unknown IDs before calling res.download.
Rank #4
Headers cannot be changed
This means the response likely started already. Move validation, authorization, and PDF generation checks before writing headers or body bytes. For streams, handle errors according to the Express or Fastify adapter and accept that a late failure may leave a partial response.
Inline mode still downloads
Verify the exact Content-Disposition value and test the browser’s PDF-viewer and download settings. inline is a presentation signal, not a guarantee that every client will render in a tab.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Or skip the browser setup
If your application needs a PDF or screenshot from a URL rather than a locally generated document, ScreenshotNeo returns a PDF from one HTTP call and can be used as the upstream source for your endpoint. Cookie banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the parameter and PDF options in the ScreenshotNeo documentation. A direct cURL request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
For a PDF, adapt the request’s output option as documented and then return the resulting bytes from your own API with Content-Type: application/pdf and your chosen disposition. ScreenshotNeo has 1,000 free shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Python and Node.js request examples
These examples show how to retrieve a generated asset from an HTTP endpoint. They use the same binary rule: write the response content as bytes, not text.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePython
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
Frequently Asked Questions
Should I return a PDF as base64 in JSON?
Not for a normal browser download. Return the PDF bytes directly with Content-Type: application/pdf; use base64 only when a separate API contract specifically requires it.
Can I change from inline viewing to downloading later?
Yes. Keep the PDF body the same and change Content-Disposition from inline to attachment, optionally supplying a filename.
What should an API return when PDF generation fails?
If no PDF bytes have been sent, return your normal error status and error body. If streaming has already begun, the client may receive a partial document, so log the failure and handle it using your framework adapter’s stream-error mechanism.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

