ScreenshotNeo

BlogHow-to

How to Send Custom HTTP Headers in Java

Add custom HTTP headers in Java with HttpClient, HttpURLConnection, or a third-party client. See runnable examples, header behavior, and fixes for common errors.

By the ScreenshotNeo team29 September 202610 min read

How to Send Custom HTTP Headers in Java

To send a custom HTTP header in Java, add it to the request before sending it. For new projects on Java 11 or later, use HttpRequest.Builder.header(name, value) with the JDK HttpClient. For existing URLConnection code, call setRequestProperty before the connection is opened. Use a third-party client when your project already depends on one or needs its additional HTTP features.

The key choice is whether a header should appear once or have multiple values. With the modern JDK client, header adds a value and setHeader replaces values already configured for that name. With HttpURLConnection, setRequestProperty sets the property and addRequestProperty adds another value. Confirm that repeated values are valid for the server before adding them.

1. Add headers with Java 11+ HttpClient

The JDK HttpClient API is available since Java 11. Build an immutable request, attach headers to its builder, and send it using send for a blocking call or sendAsync for an asynchronous call. This example is a complete Java class that makes a GET request and prints the status and response body.

The client attaches request headers before sending; the server response is a separate step to inspect.
The client attaches request headers before sending; the server response is a separate step to inspect.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class CustomHeaderGet {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/items"))
                .timeout(Duration.ofSeconds(30))
                .header("X-Request-ID", "abc-123")
                .header("Accept", "application/json")
                .GET()
                .build();

        HttpResponse<String> response = client.send(
                request, HttpResponse.BodyHandlers.ofString());

        System.out.println("Status: " + response.statusCode());
        System.out.println(response.body());
    }
}

Replace the example URL with the endpoint you call. Add each request-specific field to the builder for the request that will actually be sent. The returned status code and body tell you whether the remote service accepted the request; successful construction does not guarantee that the server recognizes a custom field.

POST with authorization and JSON

For a POST, put the headers on the same builder and select a body publisher. The content type describes the body you send; Accept describes the response format you want.

String token = System.getenv("API_TOKEN");
String json = "{\"name\":\"Ada\"}";

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/items"))
        .header("Authorization", "Bearer " + token)
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();

HttpResponse<String> response = HttpClient.newHttpClient().send(
        request, HttpResponse.BodyHandlers.ofString());

Keep credentials outside source control, for example in an environment variable or a secret manager. Do not print the authorization value when logging requests.

header, setHeader, and several headers

  • header(name, value) adds a value. Use it when multiple values for a field are intentional and supported by the endpoint.
  • setHeader(name, value) replaces previously configured values for that field. Use it when one value should be present.
  • headers(name, value, ...) accepts alternating header names and values, useful for setting several fields in one call.
HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json")
        .setHeader("X-Request-ID", "new-id")
        .headers("X-Region", "west", "X-Client", "batch-job")
        .GET()
        .build();

Header names and values must meet the builder’s requirements. Invalid input or fields restricted by the client can cause IllegalArgumentException. Let the HTTP client manage protocol fields such as Content-Length, which it can derive from the body publisher, instead of trying to override them.

Asynchronous requests

Use sendAsync when waiting synchronously would tie up the calling thread. The headers are set the same way; the result is a CompletableFuture. Handle both the response and exceptional completion.

var future = client.sendAsync(request,
        HttpResponse.BodyHandlers.ofString());

future.thenAccept(response -> {
    System.out.println("Status: " + response.statusCode());
}).exceptionally(error -> {
    System.err.println("Request failed: " + error.getMessage());
    return null;
});

Choose explicit connect and request timeouts for your service’s latency budget. A timeout bounds waiting; it does not make a remote service reliable by itself. If you add retries, make sure the operation is safe to repeat and apply a bounded retry policy.

2. Add headers with HttpURLConnection

HttpURLConnection remains useful when maintaining Java 8-era code or a codebase already built around URLConnection. Set the request method, timeouts, and request properties before an operation that may open the connection. Calls such as getInputStream can connect implicitly, after which changing setup properties is an error.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

public class LegacyHeaderGet {
    public static void main(String[] args) throws Exception {
        HttpURLConnection connection = (HttpURLConnection)
                URI.create("https://api.example.com/items")
                        .toURL().openConnection();

        connection.setRequestMethod("GET");
        connection.setRequestProperty("X-Request-ID", "abc-123");
        connection.setRequestProperty("Accept", "application/json");
        connection.setConnectTimeout(10_000);
        connection.setReadTimeout(30_000);

        int status = connection.getResponseCode();
        System.out.println("Status: " + status);

        var stream = status >= 400
                ? connection.getErrorStream()
                : connection.getInputStream();
        if (stream != null) {
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(stream, StandardCharsets.UTF_8))) {
                reader.lines().forEach(System.out::println);
            }
        }
        connection.disconnect();
    }
}

Use setRequestProperty(name, value) when one value is intended. Use addRequestProperty(name, value) only when another value for the same name is deliberate. Read the error stream for unsuccessful HTTP responses: getInputStream() can throw for an error status, while the response code and error body can explain the server’s rejection.

Sending a POST body

For a request with a body, enable output and write the bytes after setting all request properties. For JSON, set the content type explicitly.

connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("X-Request-ID", "abc-123");

byte[] body = "{\"name\":\"Ada\"}".getBytes(StandardCharsets.UTF_8);
try (var output = connection.getOutputStream()) {
    output.write(body);
}
int status = connection.getResponseCode();

Calling getOutputStream opens the connection. Configure every request property first. For non-ASCII data, choose a character encoding deliberately and ensure the content type’s charset agrees when the API expects one.

3. Choose the right Java HTTP client

Approach Java and dependencies Sending and header behavior When it fits
JDK HttpClient Java 11+; no extra dependency Blocking send and async sendAsync. header adds; setHeader replaces. Default for new JDK-based code when its configuration and features meet the need.
URLConnection/HttpURLConnection Available in older JDKs; no extra dependency Primarily imperative and blocking. Set properties before connecting; addRequestProperty adds another value. Java 8 maintenance or existing URLConnection designs.
Third-party client Requires a dependency; exact support depends on library and version Methods and sync/async behavior vary. Check the version’s documentation for replace versus append semantics. When already standardized in the application or when broader client configuration is needed.

Apache HttpClient is one third-party option. Its older 3.1 API documents setRequestHeader/setHeader for replacement and addRequestHeader/addHeader for adding another instance, but that API is marked deprecated. Do not copy an old example into a current project without checking the documentation for the version actually installed. Similar-looking method names across libraries do not guarantee identical behavior.

4. cURL, Python, and Node.js equivalents

These examples help compare the same request across tools. Use the endpoint’s actual header names and values; the example request ID is not an authentication credential.

cURL

curl -i \
  -H 'X-Request-ID: abc-123' \
  -H 'Accept: application/json' \
  'https://api.example.com/items'

Python with requests

import requests

response = requests.get(
    "https://api.example.com/items",
    headers={
        "X-Request-ID": "abc-123",
        "Accept": "application/json",
    },
    timeout=30,
)
print(response.status_code)
print(response.text)

Node.js fetch

const response = await fetch('https://api.example.com/items', {
  headers: {
    'X-Request-ID': 'abc-123',
    'Accept': 'application/json',
  },
  signal: AbortSignal.timeout(30_000),
});
console.log(response.status);
console.log(await response.text());

Do not assume duplicate header handling is the same in every language or HTTP library. For fields that allow multiple values, follow the server’s expected representation and the client’s documented rules.

5. Common errors and fixes

Symptom Likely cause Fix
IllegalArgumentException while building a request A name or value is invalid, or the client restricts a field it manages. Check for malformed names, prohibited characters, and protocol-controlled headers. Let the client generate fields such as content length.
Header seems to disappear or has an unexpected value A later call replaced it, or the code built a different request than the one sent. Keep header setup and send logic together; use header for intentional additional values and setHeader for replacement.
URLConnection says properties cannot be changed A call such as getInputStream, getOutputStream, or connect already opened the connection. Move method, timeout, and header configuration before any operation that can connect.
Server returns 401 or 403 Authentication is missing, expired, malformed, or lacks permission; a custom field may not be the credential mechanism the API expects. Check the API’s authentication instructions, token scope, and exact header format. Never include secrets in logs or support output.
Server returns 400 or ignores the custom field The field name/value is wrong, not supported, or duplicated in a way the endpoint does not accept. Compare against the endpoint documentation and inspect response status and body. Remove duplicate values unless the API explicitly supports them.
Timeout or connection exception DNS, TLS, network reachability, server latency, or a timeout below the request’s real needs. Distinguish connect timeout from overall request/read timeout, verify the host and certificate path, and set bounded values appropriate to the operation.
Error body is missing The code reads only the normal input stream on a failed response. With HttpURLConnection, inspect the status and read getErrorStream() when present.

6. Reliability, performance, and security

  • Reuse the client: Build an HttpClient for the service or application component and reuse it rather than rebuilding one for every request. Keep request-specific headers on each request.
  • Set bounded timeouts: Choose connection and request/read limits based on the endpoint and caller’s latency budget. Handle timeout exceptions as a distinct failure.
  • Retry carefully: A retry can duplicate a write. Retry only operations that are safe to repeat or protected by the API’s idempotency mechanism, and cap attempts.
  • Keep header values clean: Do not allow untrusted input to construct arbitrary header names or values. Validate values against the API’s expected format.
  • Protect secrets: Treat authorization, cookies, and API keys as secrets. Redact them from logs, traces, exceptions, and request dumps.
  • Avoid unnecessary fields: Send only fields the endpoint needs. Extra or conflicting headers can cause rejection and make debugging harder.

Header configuration itself has negligible cost compared with network latency and response processing. The practical performance choices are client reuse, concurrency limits, response-body handling, and sensible timeouts. For large responses, use a streaming body handler or stream rather than collecting the entire body in memory.

7. Capture a page with custom headers

Custom headers are useful when an HTTP endpoint needs a request ID, authentication, or content negotiation. For website screenshots, a browser-based capture is a different job: the service must load and render the page, including its browser-visible state. You can build that browser workflow yourself, or use a screenshot API.

A screenshot workflow has to render the page in a browser, then capture its visible state.
A screenshot workflow has to render the page in a browser, then capture its visible state.

DIY checklist

  1. Choose a Java browser automation library and browser runtime compatible with your project.
  2. Set extra HTTP headers through that library’s documented page or context API before navigating.
  3. Navigate to the target, wait for the page condition you need, then capture a full page or selected element.
  4. Check how redirects, cross-origin requests, service workers, cookies, and browser-managed headers affect the final request.
  5. Close pages and browser resources reliably, and bound navigation and capture time.

There is no universal browser automation header method: APIs differ by library and version, and headers applied to one navigation may not automatically apply to every subresource or redirected origin. Use the selected library’s official documentation for the exact method and verify the behavior against the target application.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its GET endpoint returns a PNG, JPEG, WebP, or PDF. See the API documentation for request options, including custom headers.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
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(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, and failed loads are never billed, and the response identifies the page verdict and billing status in headers. Its MCP server gives AI agents tools for screenshots, page info, and PDF capture. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is available on every plan.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

FAQ

Can I use custom headers with Java 8?

Yes. Use HttpURLConnection.setRequestProperty and configure it before connecting. The JDK HttpClient approach in this guide requires Java 11 or later.

Are HTTP header names case-sensitive?

HTTP field names are conventionally treated case-insensitively by HTTP implementations. Use the spelling shown in the API documentation for clarity, and do not use capitalization changes to distinguish fields.

Can I set every HTTP header manually?

No. Clients may manage or restrict some fields, and protocol values such as content length should generally be derived by the client from the request body. Follow the JDK client and endpoint documentation for fields under client control.

Does setting a header prove the server received it?

No. It proves only that your client accepted the request configuration. Proxies, redirects, server policy, and endpoint behavior can affect what is received or used. Validate through the API’s response or server-side diagnostics.