Recommended Free Tools
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Read a PDF as binary data and return those bytes through your framework’s response API. Set the response media type to application/pdf, then use Content-Disposition: inline to signal that the browser should display it or attachment to signal a download. Choose a buffered response for a suitably sized PDF already in memory, a framework file-serving method for a trusted server-side file, or a stream when the document is produced incrementally. Never let a request supply an unrestricted filesystem path.
What the HTTP response needs
A PDF response has two separate parts: the body, which contains the PDF’s binary bytes, and headers, which describe how the client should handle them. Do not decode the bytes as text or put them in a JSON string unless your API deliberately defines a separate encoding format. For a normal PDF response, send the file bytes directly and set Content-Type: application/pdf.
Content-Disposition communicates the intended presentation. Use inline when the response is intended for an in-browser preview. Use attachment when it is intended as a download; a filename can be included so the client has a suggested name. These headers express intent, not a guarantee that every browser will display the PDF: the client’s capabilities and settings also matter.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →The framework should construct the response rather than your application manually assembling an HTTP message. Its file and stream APIs can handle response metadata and transfer details. Check the API for the framework and version you actually deploy; the examples below show the approaches documented for Flask, Express 4.x, and NestJS.
#1 Best Overall
Choose how to deliver the PDF
| PDF source | Suitable approach | Trade-off to consider |
|---|---|---|
| Already in memory as bytes | Wrap the bytes in a binary-mode file-like object and pass it to the framework’s file response API. | The application already holds the document in memory; use this for an appropriately sized PDF. |
| A trusted file on the server | Use the framework’s file-serving or download method with a server-controlled path. | Do not derive an unrestricted path from request input. File-serving APIs can manage file metadata and transfer behavior. |
| Generated or retrieved incrementally | Return a framework-supported stream response. | Streaming can avoid collecting the entire PDF in application memory, but failures after transmission begins may leave the client with a partial response. |
Return in-memory PDF bytes with Flask
Flask’s send_file accepts either a filesystem path or a file-like object. For bytes already in memory, wrap them in io.BytesIO, which provides a binary file-like object, and position its read pointer at the beginning before sending it. Flask’s API exposes mimetype, as_attachment, and download_name for the response type and download behavior.
from io import BytesIO
from flask import Flask, send_file
app = Flask(__name__)
@app.get("/report.pdf")
def report_pdf():
# Replace this with bytes returned by your PDF generator or storage layer.
pdf_bytes = build_report_pdf()
pdf_file = BytesIO(pdf_bytes)
pdf_file.seek(0)
return send_file(
pdf_file,
mimetype="application/pdf",
as_attachment=False,
download_name="report.pdf",
)
This example chooses inline presentation. To signal a download instead, set as_attachment=True; keep download_name if you want to suggest a filename. The function build_report_pdf() represents your existing PDF-generation code, so replace it with the function or storage lookup used by your application. The important boundary is that the result is bytes, not a text-mode file.
When the document is too large to buffer
An in-memory response is straightforward, but the whole PDF is present in application memory before it is sent. If your PDF generator or upstream source can produce data incrementally, use a stream-capable response instead. For a trusted file on disk, Flask’s documentation prefers paths in most cases; use its file-serving API rather than reading a large file into a byte string without a reason.
Serve a trusted file with Express 4.x
Express provides res.download(path, filename, options, callback) for sending a file as a download. Keep the path under server control. If a user’s request selects which document to receive, map a validated identifier to an authorized file; do not concatenate a raw request value into a filesystem path. Express documents a root option for constraining paths, and its API warns about paths influenced by user input.
app.get("/reports/:reportId/download", (req, res, next) => {
const reportPath = lookupAuthorizedReportPath(req.params.reportId);
if (!reportPath) {
return res.sendStatus(404);
}
res.type("application/pdf");
res.download(reportPath, "report.pdf", (err) => {
if (err) {
// The response may already have started; do not assume a new
// error response can replace a partial file transfer.
if (res.headersSent) {
return next(err);
}
return next(err);
}
});
});
lookupAuthorizedReportPath is intentionally application-specific: it should verify that the report exists and that the current user may access it, then return a trusted path. The example sets the media type explicitly and uses res.download to request download behavior with a suggested filename. Use the options documented for your Express version when you need path constraints or other transfer options.
File transfer errors need special care. The callback can report a failure, but if headers or part of the body have already been sent, the server may not be able to replace the response with a clean JSON error or status page. Handle that case according to your error middleware and deployment behavior; do not assume that a failed transfer can always be turned into a fresh response.
Stream a PDF with NestJS
NestJS documents StreamableFile for returning streams. It accepts response metadata options such as content type, content disposition, and length. This fits PDFs generated or obtained incrementally, where buffering the complete document in application memory is undesirable.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteimport { Controller, Get, StreamableFile } from "@nestjs/common";
import { createReadStream } from "node:fs";
@Controller("reports")
export class ReportsController {
@Get("report.pdf")
getReport(): StreamableFile {
const stream = createReadStream(getTrustedReportPath());
return new StreamableFile(stream, {
type: "application/pdf",
disposition: 'inline; filename="report.pdf"',
});
}
}
getTrustedReportPath() stands for a server-controlled path lookup; do not replace it with an unchecked request path. To signal download behavior, use an attachment disposition instead of inline. Include a length only when your application knows the correct length and can provide it. NestJS documents differences in stream error handling for its Express and Fastify adapters, so check the adapter-specific guidance and account for whether headers or body bytes have already been sent before responding to an error.
Set disposition and filename deliberately
- For an in-browser preview: set
Content-Type: application/pdfandContent-Disposition: inline. Do not promise a preview on clients that do not support one. - For a download: set the same PDF media type and use
Content-Disposition: attachmentwith a sensible suggested filename. In Flask, useas_attachment=Trueanddownload_name; in Express, use its download method and filename argument; in NestJS, supply an attachment disposition. - For generated filenames: take care not to let untrusted input create unsafe paths or response metadata. A download filename is metadata, not a reason to use a request-provided value as a filesystem path.
Use the framework’s response API to set these headers rather than manually writing a second response after the file transfer starts. The exact parameter names and defaults differ by framework and version.
Handle failures before and during a transfer
Separate errors that occur before the response starts from errors that happen while bytes are being sent. Before transmission, your application can normally choose an appropriate error response. After headers or body data have gone to the client, replacing the partial PDF with a normal error body may no longer be possible. Express and NestJS document transfer or stream error handling, including adapter-dependent behavior for NestJS.
- Validate access and locate the document first. Resolve the requested report through an authorization-aware lookup and confirm it is available before starting the transfer.
- Set response metadata before sending data. Use the PDF media type and the intended disposition through your framework’s API.
- Handle transfer errors according to response state. If the response has already started, use the framework’s documented stream/error path rather than trying to send a second response body.
Troubleshooting common PDF response problems
The browser displays unreadable characters or downloads a file it cannot open
Check that the body is the original PDF bytes and the response media type is application/pdf. In Flask, use a binary-mode object such as BytesIO, not a text-mode stream. If the bytes were generated in memory, confirm the file-like object is positioned at the start before sending it.
The PDF downloads instead of previewing
Check the disposition. attachment signals a download; use inline for the intended preview behavior. The header does not override a client’s own PDF handling settings or capabilities.
Rank #4
The downloaded file has an unexpected name
Set the filename intentionally using the framework’s supported parameter or disposition option. For Flask, that is download_name; for Express, it is the filename argument to res.download. Verify the exact API for the framework version in use.
A request can reach files outside the intended directory
Do not pass user-controlled paths directly to a file-sending method. Resolve a permitted identifier to a trusted file, enforce authorization, and use the framework’s path constraint where available. Express documents a root option; Flask explicitly warns against passing user-provided paths to send_file.
The client receives a partial PDF after an error
A failure can happen after the server has begun writing the response. At that point, a normal error document may not be possible. Check your framework’s transfer callback or stream error guidance and its adapter behavior, and avoid treating a partially sent response like an untouched request.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC 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 & 11Performance, reliability, and cost considerations
There is no single fastest response method for every PDF. Buffered bytes are convenient when the document already exists in memory and is appropriately sized. A trusted path lets the framework manage file transfer and metadata; Flask says paths are preferred in most cases. A stream can avoid holding the entire document in application memory when it is generated or retrieved incrementally.
Streaming changes failure handling as well as memory use. Plan for the possibility that generation or retrieval fails after transmission has begun. For a path-based response, validate the file and access rights before starting. For an in-memory response, account for the PDF’s size in the memory used by the application. The framework documentation cited for this topic does not establish performance percentages or a universal size threshold, so choose based on how your application produces the PDF and measure it in your own deployment.
Or skip the browser setup
If what you need is a clean capture of a webpage—not serving an existing PDF file—ScreenshotNeo is a separate option: one GET request accepts a URL and returns a screenshot or PDF. It does not replace the framework response code above for returning your own PDF bytes.
Quick Recap
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for the API. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card, with paid plans starting at $5 for 3,000. Sign up for the free plan.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →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.

