Request Customization

About

RestTemplate provides several hooks and constructs to allow request customization a critical aspect when interacting with external APIs or internal microservices that require authentication, custom headers, request transformations, logging, tracing, or advanced configuration like timeouts and interceptors.

Customizing a request ensures that our API calls are:

  • Secure (e.g., with bearer tokens or API keys),

  • Compliant (e.g., with required content-type or correlation IDs),

  • Reliable (e.g., with retries, timeouts, and fallbacks),

  • Traceable (e.g., with custom headers for distributed tracing).

Common Use Cases

Use Case

Customization Example

Add Path and Query Param

Set Query, Path parameters

Add Request Body

Set API Request body

Add authentication headers

Bearer token, API key, Basic Auth

Include trace/correlation IDs

Pass unique request ID for observability

Change content type / accept type

application/json, application/xml, custom media types

Modify request payload

Pre-serialize object, or add additional request fields

Set timeouts or connection pool

Customize via underlying HttpClient or RestTemplateBuilder

Pre-process request before sending

Interceptors, ClientHttpRequestInterceptor

Add dynamic headers from context

Extract values from MDC, ThreadLocal, or SecurityContext

Add Path and Query Param

When interacting with external or internal REST APIs, it is common to dynamically construct the request URL by appending:

  • Path Parameters — values embedded directly within the URL path (e.g., /user/{id}),

  • Query Parameters — key-value pairs appended after the ? in the URL (e.g., ?status=active&limit=10).

Spring’s RestTemplate provides multiple ways to inject these parameters cleanly and maintainably.

1. Using Path Variables with URI Template Expansion

This is the most common and safe way to replace path variables in endpoint templates using placeholders.

String url = "https://api.example.com/users/{userId}/orders/{orderId}";
Map<String, String> uriVariables = Map.of(
    "userId", "12345",
    "orderId", "987"
);

ResponseEntity<OrderResponse> response = restTemplate.getForEntity(
    url,
    OrderResponse.class,
    uriVariables
);
  • Path placeholders ({userId}, {orderId}) are automatically replaced by values from the Map.

  • No need for string concatenation.

  • Prevents URL injection or malformed paths.

2. Path Params via UriComponentsBuilder for Dynamic URIs

Use UriComponentsBuilder when we want to construct complex URLs that include both path variables and query parameters dynamically.

URI uri = UriComponentsBuilder
    .fromUriString("https://api.example.com/users/{userId}/orders")
    .queryParam("page", 1)
    .queryParam("limit", 50)
    .queryParam("sortBy", "date")
    .buildAndExpand("12345")
    .toUri();

ResponseEntity<OrderList> response = restTemplate.getForEntity(uri, OrderList.class);
  • buildAndExpand("12345") replaces {userId}.

  • queryParam() is used for clean and safe appending of query parameters.

  • Works great for pagination, filtering, and sorting APIs.

3. Fully Programmatic Path + Query Construction

This is helpful when query params are optional or dynamically built based on logic:

MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("category", "books");
queryParams.add("sort", "title");
queryParams.add("sort", "price"); // multiple values for same key

URI uri = UriComponentsBuilder
    .fromUriString("https://api.example.com/users/{userId}/cart")
    .queryParams(queryParams)
    .buildAndExpand("u7890")
    .encode()
    .toUri();

ResponseEntity<CartResponse> response = restTemplate.exchange(
    uri,
    HttpMethod.GET,
    null,
    CartResponse.class
);
  • MultiValueMap handles multiple query parameters with the same key.

  • encode() ensures proper URI encoding for special characters.

4. Mixing Path and Query Params in POST Request

Even in POST calls (or PUT), query parameters are often used for metadata like versioning or flags.

URI uri = UriComponentsBuilder
    .fromUriString("https://api.example.com/users/{id}/upload")
    .queryParam("async", "true")
    .queryParam("version", "v2")
    .buildAndExpand("user123")
    .toUri();

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<FileUploadRequest> request = new HttpEntity<>(fileRequest, headers);

ResponseEntity<FileUploadResponse> response = restTemplate.exchange(
    uri,
    HttpMethod.POST,
    request,
    FileUploadResponse.class
);

Add Request Body

When making HTTP methods like POST, PUT, or PATCH, the client often needs to send a structured request body typically in JSON or XML format. This body carries the actual business payload (e.g., user details, payment data, file metadata).

Spring’s RestTemplate allows for seamless serialization of Java objects into the body of a request using its built-in message converters (usually MappingJackson2HttpMessageConverter for JSON).

1. POST Request with JSON Body

UserCreateRequest user = new UserCreateRequest("john.doe", "password123");

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<UserCreateRequest> requestEntity = new HttpEntity<>(user, headers);

ResponseEntity<UserResponse> response = restTemplate.postForEntity(
    "https://api.example.com/users",
    requestEntity,
    UserResponse.class
);
  • user is automatically serialized into JSON.

  • The content-type is declared explicitly.

  • Response is mapped to UserResponse.

2. PUT Request with Request Body

UserUpdateRequest updateRequest = new UserUpdateRequest("john.doe", "[email protected]");

HttpEntity<UserUpdateRequest> entity = new HttpEntity<>(updateRequest);

restTemplate.put("https://api.example.com/users/{id}", entity, "123");
  • PUT operations use restTemplate.put().

  • Path variable ({id}) is passed separately.

3. Exchange Method for Full Control (POST, PUT, PATCH)

For advanced use cases like:

  • Passing custom headers

  • Supporting non-JSON content

  • Handling dynamic URLs

Use exchange() method:

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Correlation-Id", "txn-456789");

PaymentRequest payload = new PaymentRequest("user123", 100.0);

HttpEntity<PaymentRequest> request = new HttpEntity<>(payload, headers);

URI uri = UriComponentsBuilder
    .fromUriString("https://api.example.com/payments?async=true")
    .build()
    .toUri();

ResponseEntity<PaymentResponse> response = restTemplate.exchange(
    uri,
    HttpMethod.POST,
    request,
    PaymentResponse.class
);

4. Sending a List or Collection in Request Body

For APIs that accept arrays or lists:

List<String> userIds = List.of("u123", "u124", "u125");

HttpEntity<List<String>> request = new HttpEntity<>(userIds);

ResponseEntity<GroupResponse> response = restTemplate.postForEntity(
    "https://api.example.com/groups/{groupId}/users",
    request,
    GroupResponse.class,
    "teamA"
);

5. Sending a Form-Encoded Body (Non-JSON)

If the backend expects a URL-encoded form body:

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", "john");
map.add("password", "secret");

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

ResponseEntity<TokenResponse> response = restTemplate.postForEntity(
    "https://auth.example.com/oauth/token",
    request,
    TokenResponse.class
);

Add authentication headers

In most enterprise applications, outbound HTTP requests need to include authentication details—whether via Bearer tokens, API keys, Basic Auth, or custom headers—to access protected APIs.

Spring’s RestTemplate allows injecting headers easily via HttpHeaders, which are then bundled with the request body using HttpEntity.

1. Bearer Token Authentication

Used widely with OAuth 2.0, JWT, and internal token-based auth:

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...");

HttpEntity<Void> entity = new HttpEntity<>(headers);

ResponseEntity<AccountResponse> response = restTemplate.exchange(
    "https://api.example.com/accounts/{id}",
    HttpMethod.GET,
    entity,
    AccountResponse.class,
    "acc123"
);
  • setBearerAuth() automatically adds the header: Authorization: Bearer <token>

2. Basic Authentication

For services still using basic HTTP authentication:

String plainCreds = "username:password";
String base64Creds = Base64.getEncoder().encodeToString(plainCreds.getBytes());

HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Creds);

HttpEntity<Void> entity = new HttpEntity<>(headers);

ResponseEntity<EmployeeData> response = restTemplate.exchange(
    "https://internal.example.com/employees",
    HttpMethod.GET,
    entity,
    EmployeeData.class
);
  • Encodes the string as Authorization: Basic <encoded-credentials>

3. API Key as Header

Some services use API keys passed in custom headers like X-API-Key or x-client-secret:

HttpHeaders headers = new HttpHeaders();
headers.add("X-API-Key", "api-key-987654321");

HttpEntity<Void> request = new HttpEntity<>(headers);

ResponseEntity<InvoiceResponse> response = restTemplate.exchange(
    "https://thirdparty.com/invoices",
    HttpMethod.GET,
    request,
    InvoiceResponse.class
);

4. With Request Body and Auth Headers (POST)

When sending both payload and headers:

PaymentRequest payment = new PaymentRequest("txn789", 150.00);

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth("secure-token-123");
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<PaymentRequest> requestEntity = new HttpEntity<>(payment, headers);

ResponseEntity<PaymentResponse> response = restTemplate.postForEntity(
    "https://api.example.com/payments",
    requestEntity,
    PaymentResponse.class
);

5. Dynamically Inject Headers via Interceptor (Reusable)

If every request should carry a token:

public class AuthInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {
        request.getHeaders().setBearerAuth("dynamic-token-here");
        return execution.execute(request, body);
    }
}

Register it:

RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new AuthInterceptor());
  • Recommended when a common token applies to all requests.

  • Encouraged over repeating header logic in every request.

Include trace/correlation IDs

In distributed systems and microservices, correlation IDs (also called trace IDs or request IDs) are essential for request tracking, observability, debugging, and log correlation across service boundaries.

When making HTTP calls using RestTemplate, it's a common practice to propagate the correlation ID received in the incoming request to any downstream services.

1. Manually Add a Correlation ID to Headers

If the correlation ID is generated earlier (e.g., in a filter or controller), inject it into outgoing request headers.

String correlationId = MDC.get("X-Correlation-ID"); // From thread-local logging context

HttpHeaders headers = new HttpHeaders();
headers.add("X-Correlation-ID", correlationId);

HttpEntity<Void> entity = new HttpEntity<>(headers);

ResponseEntity<CustomerResponse> response = restTemplate.exchange(
    "https://api.partner.com/customers/{id}",
    HttpMethod.GET,
    entity,
    CustomerResponse.class,
    "cust-101"
);
  • Header Name: Use a consistent name like X-Correlation-ID or traceId.

2. Automatically Inject Correlation ID via Interceptor

To avoid repeating this logic everywhere, use a ClientHttpRequestInterceptor:

public class CorrelationIdInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(
            HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        
        String correlationId = MDC.get("X-Correlation-ID");
        if (correlationId != null) {
            request.getHeaders().add("X-Correlation-ID", correlationId);
        }

        return execution.execute(request, body);
    }
}

Register the interceptor:

@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.getInterceptors().add(new CorrelationIdInterceptor());
    return restTemplate;
}
  • Now every outbound call via RestTemplate will carry the correlation ID.

3. Integration with Spring Cloud Sleuth

If we use Spring Cloud Sleuth, it automatically manages trace IDs and span IDs:

X-B3-TraceId: 7b63a1d95e0e33c7
X-B3-SpanId: d7a1fbea4e3fa0b7
X-B3-Sampled: 1

We can keep this in sync with our custom X-Correlation-ID by configuring MDC.

4. Handling Missing Correlation IDs

In a gateway or edge service, it’s common to generate a correlation ID if not already present:

String correlationId = Optional.ofNullable(MDC.get("X-Correlation-ID"))
                               .orElse(UUID.randomUUID().toString());

MDC.put("X-Correlation-ID", correlationId);

Then pass this into headers downstream.

Change content type / accept type

When consuming external or internal REST APIs, it's crucial to explicitly set Content-Type and Accept headers to align with what the server expects and returns. These headers define:

  • Content-Type: The format of the request body being sent.

  • Accept: The format that the client can process in the response.

Failing to set these correctly can lead to:

  • 415 Unsupported Media Type

  • 406 Not Acceptable

  • Unexpected deserialization failures

Typical Scenarios

Use Case
Required Content-Type
Required Accept-Type

Sending JSON payload

application/json

application/json

Consuming XML-based third-party API

application/xml

application/xml

Posting form data

application/x-www-form-urlencoded

Depends on API, often JSON or text

Multipart file upload

multipart/form-data

Usually JSON or plain text

Consuming text or CSV file

text/plain, text/csv

text/plain, text/csv

1. Set Content-Type and Accept-Type Explicitly

We can customize both headers using HttpHeaders:

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

MyRequest requestPayload = new MyRequest("sample", 42);
HttpEntity<MyRequest> requestEntity = new HttpEntity<>(requestPayload, headers);

ResponseEntity<MyResponse> response = restTemplate.exchange(
    "https://partner.api.com/process",
    HttpMethod.POST,
    requestEntity,
    MyResponse.class
);

2. Changing Content-Type for Form Submission

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("username", "john.doe");
form.add("password", "securePass");

HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(form, headers);

ResponseEntity<String> response = restTemplate.postForEntity(
    "https://auth.example.com/login",
    entity,
    String.class
);

3. Accepting XML Response from External Service

HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_XML));

HttpEntity<Void> entity = new HttpEntity<>(headers);

ResponseEntity<MyXmlResponse> response = restTemplate.exchange(
    "https://legacy.service.com/data.xml",
    HttpMethod.GET,
    entity,
    MyXmlResponse.class
);

Note: Our ObjectMapper (or JAXB if XML) must be correctly configured to handle XML.

4. Combine with Interceptors for Reusability

In large systems, we can create an interceptor that dynamically adjusts content negotiation based on context or configuration.

public class MediaTypeInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        request.getHeaders().setAccept(List.of(MediaType.APPLICATION_JSON));
        return execution.execute(request, body);
    }
}

Register globally:

@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.getInterceptors().add(new MediaTypeInterceptor());
    return restTemplate;
}

5. Accept Multiple Media Types

Sometimes, services support multiple response formats:

headers.setAccept(Arrays.asList(
    MediaType.APPLICATION_JSON,
    MediaType.APPLICATION_XML
));

This makes the request more flexible — the server can return what it prefers among those listed.

Reference: Required Dependencies & Import Statements

Maven Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Common Import Statements

Below are the commonly used import statements for the examples in this guide:

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.RestClientException;

import java.util.Base64;
import java.util.Map;
import java.util.Collections;

// For interceptors
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;

Last updated