Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Return the document as an ASP.NET Core file result, not as JSON. In a controller, use ControllerBase.File with application/pdf and a suggested filename; pass a byte[] for an in-memory document or a Stream for stream-backed content. In a Minimal API, use TypedResults.File. These responses let HTTP clients recognize the payload as a PDF and save or display it according to their own behavior.

Return PDF bytes from a controller

When your generator has completed the PDF in memory, return the byte array directly:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/reports")]
public sealed class ReportsController : ControllerBase
{
    [HttpGet("report")]
    public IActionResult GetReport()
    {
        byte[] pdf = GenerateReport();
        return File(pdf, "application/pdf", "report.pdf");
    }

    private static byte[] GenerateReport()
    {
        // Replace this with your PDF-generation code.
        throw new NotImplementedException();
    }
}

The three arguments are the PDF bytes, the media type, and the suggested download name. The byte-array overload creates a FileContentResult. Microsoft documents this pattern in its ASP.NET Core response guidance and the ControllerBase.File API reference.

Why application/pdf matters

application/pdf is the registered media type that identifies the response body as a PDF. Do not send the bytes as a normal JSON property or convert them to Base64 unless a particular client protocol explicitly requires that representation. A file result writes the binary body and the appropriate response metadata for you.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use an explicit filename

report.pdf is a suggested name, not a promise that every browser or HTTP client will use it identically. A client may display the document, prompt for a name, or apply its own download policy. Keep the name safe and derived from trusted, validated values when it includes user or database data.

Return a stream when the PDF is stream-backed

If your PDF library, object storage client, or file system naturally supplies a stream, avoid first copying it into a second byte array solely to call the API. Return the stream overload:

[HttpGet("download")]
public IActionResult Download()
{
    Stream pdfStream = OpenPdfStream();
    return File(pdfStream, "application/pdf", "report.pdf");
}

private static Stream OpenPdfStream()
{
    // Return a readable stream positioned at the beginning of the PDF.
    throw new NotImplementedException();
}

This overload creates a FileStreamResult. ASP.NET Core disposes the supplied stream after the response is sent, so do not wrap it in a using statement that ends before the action returns. The stream must remain readable while the response is being written.

Byte array or stream?

Situation Use Result type Important consideration
The completed PDF is already a byte[] File(bytes, "application/pdf", "name.pdf") FileContentResult Simple and direct; the entire document is already materialized.
The source supplies a readable stream File(stream, "application/pdf", "name.pdf") FileStreamResult Keep the stream alive until response execution completes; ASP.NET Core disposes it afterward.

There is no universal size threshold established by the API documentation. Base the choice on how your generator or storage layer exposes the document and on the memory and lifetime characteristics of your application.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Minimal API version

For an ASP.NET Core Minimal API endpoint, return a typed file result:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/report", () =>
{
    byte[] pdf = GenerateReport();
    return TypedResults.File(pdf, "application/pdf", "report.pdf");
});

app.Run();

static byte[] GenerateReport()
{
    // Replace this with your PDF-generation code.
    throw new NotImplementedException();
}

The representation is the same: PDF bytes, the application/pdf content type, and an optional suggested filename. Choose ControllerBase.File in controller actions and TypedResults.File in Minimal API handlers, matching the application style you are already using.

Enable range processing only when you need it

ControllerBase.File also has overloads with an enableRangeProcessing argument. Enabling it allows HTTP range requests and the corresponding 206 Partial Content and 416 Range Not Satisfiable responses described in the API reference:

[HttpGet("large-report")]
public IActionResult LargeReport()
{
    Stream pdfStream = OpenLargeReportStream();
    return File(
        pdfStream,
        "application/pdf",
        "large-report.pdf",
        enableRangeProcessing: true);
}

Range support is optional, not a requirement for ordinary PDF downloads. Turn it on when your endpoint needs resumable or partial retrieval and the underlying stream can support the access pattern. Otherwise use the simpler overload.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Headers and endpoint behavior to verify

  • Content-Type: should be application/pdf.
  • Content-Disposition: supplying a filename asks the framework to provide a download name; the consuming client decides whether to download or display it.
  • Body: must contain the raw PDF bytes, beginning with the PDF data produced by your generator, not a JSON wrapper.
  • Status: a successful file result is normally a 200 response; range-enabled requests can produce 206 or 416 as documented by Microsoft.

Authentication and authorization still belong on the endpoint. Apply your normal controller attributes or Minimal API authorization policies before generating or opening a document, especially when the filename or source path is based on a request value.

Call the endpoint from common clients

cURL

curl -fS -H "Accept: application/pdf" 
  "https://api.example.com/api/reports/report" 
  -o report.pdf

The -o option writes the binary response to a file. -fS makes HTTP failures visible instead of silently saving an error response as if it were a PDF.

Python

import requests

response = requests.get(
    "https://api.example.com/api/reports/report",
    headers={"Accept": "application/pdf"},
    timeout=90,
)
response.raise_for_status()
with open("report.pdf", "wb") as file:
    file.write(response.content)

For a very large response, use stream=True and write chunks while iterating over response.iter_content; that changes the client-side buffering strategy, not the ASP.NET Core response type.

Node.js

const res = await fetch('https://api.example.com/api/reports/report', {
  headers: { Accept: 'application/pdf' }
});

if (!res.ok) {
  throw new Error(`HTTP ${res.status}`);
}

const bytes = Buffer.from(await res.arrayBuffer());
await require('node:fs').promises.writeFile('report.pdf', bytes);

Testing the response

Test both the HTTP metadata and the file body. A useful integration test requests the route, asserts a successful status, checks that the content type is PDF, and verifies that the response body is non-empty. If your test can inspect the generated bytes, also verify that they are the exact output expected from your PDF library. Test an unauthorized request separately so an authentication error cannot be mistaken for a malformed PDF.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
[Fact]
public async Task Report_returns_a_pdf()
{
    await using var factory = new WebApplicationFactory<Program>();
    using var client = factory.CreateClient();

    using HttpResponseMessage response =
        await client.GetAsync("/api/reports/report");

    response.EnsureSuccessStatusCode();
    Assert.Equal("application/pdf", response.Content.Headers.ContentType?.MediaType);

    byte[] body = await response.Content.ReadAsByteArrayAsync();
    Assert.NotEmpty(body);
}

Adapt the test host and authentication setup to your application. The important assertions are that the endpoint returns a file response and that the content type and bytes are correct.

Troubleshooting common failures

The client receives JSON instead of a PDF

Check that the action returns File(...) or TypedResults.File(...) directly. Returning an object such as new { pdf } invokes JSON serialization. If the PDF generator reports an error, your exception middleware may also return a JSON problem document; inspect the status code and content type before saving the body.

The downloaded file is zero bytes or unreadable

Confirm that the byte array contains the completed document and that a stream is positioned at its beginning. Do not close or dispose a stream before the framework has written the response. Also make sure your generator has finished writing and flushing its output before returning the file result.

The response says it is an octet stream

Pass "application/pdf" as the content type. A generic type such as application/octet-stream does not identify the document as specifically as the PDF media type.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The filename is not what the caller expected

The filename argument is a suggestion. Inspect the response headers at the client and account for client-specific download behavior. If you build the name from request data, validate it and avoid path separators or control characters.

Range requests fail

Range processing is disabled unless you opt in through the appropriate file-result overload. If you enable it, confirm that the requested range is valid and that the source stream supports the way your application serves it. A 416 response indicates an unsatisfiable range, not necessarily a bad PDF.

A stored-file endpoint seems unnecessarily complex

Microsoft’s Minimal API guidance notes that path-based file results are less common when static-file middleware can serve public assets. Use an API file result when authorization, routing, auditing, or other application logic must run; otherwise evaluate whether static file serving is the better fit.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If the PDF you need is a screenshot or PDF capture of a web page, ScreenshotNeo returns the file from one request instead of requiring you to install and manage a browser. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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 API documentation for output and option details. The service also supports full-page captures with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets and custom viewports, retina scale, PDF paper and margin controls, HTML/CSS rendering, custom JavaScript and CSS, pre-capture clicks, hidden selectors, selector or network-idle waits, request and resource blocking, custom headers/cookies/user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, TTL-based caching, signed image links, asynchronous jobs with signed webhooks, up to 100 URLs per bulk call, a usage API, and an OpenAPI specification.

A free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.

Practical implementation checklist

  • Generate the PDF completely before returning the result.
  • Return a framework file result rather than a JSON object containing binary data.
  • Set application/pdf as the content type.
  • Provide a safe suggested filename when a download name is useful.
  • Choose the byte-array overload for materialized bytes and the stream overload for stream-backed content.
  • Keep a returned stream open until response execution completes.
  • Enable range processing only when the endpoint requires it.
  • Test status, content type, body bytes, and authorization behavior with an HTTP client.

Official API references

For the complete overload lists and version-specific signatures, consult Microsoft’s Create responses in Minimal API applications and ControllerBase.File Method documentation.

Frequently Asked Questions

Can I return a PDF from a POST action instead of GET?

Yes. The HTTP method does not change the file-result pattern; return the same byte-array or stream file result after processing the posted input.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Do I need a special PDF response class?

No. ASP.NET Core’s built-in controller and Minimal API file results carry the PDF bytes or stream and response metadata.

Should I enable range processing for every PDF endpoint?

No. It is an optional capability; enable it only when your endpoint needs HTTP range requests.

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.