Series MapLesson 80 / 112
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Deepen PracticeOrdered learning track

HTTP Client Patterns and Jersey Client

Java HTTP Client Patterns and Jersey Client

Outbound HTTP integration patterns for Java/JAX-RS services, including Jersey Client lifecycle, connection pooling, timeouts, filters, entity handling, and production failure modes

13 min read2560 words
PrevNext
Lesson 80112 lesson track62–92 Deepen Practice
#http-client#jersey-client#integration#jax-rs+3 more

Part 080 — Java HTTP Client Patterns and Jersey Client

Fokus part ini: memahami outbound HTTP client sebagai dependency boundary, bukan sekadar client.target(url).request().get().

JAX-RS sering diasosiasikan dengan server-side endpoint.

Tetapi di enterprise backend, service juga sering menjadi HTTP client:

JAX-RS resource
  -> application service
  -> integration adapter
  -> outbound HTTP client
  -> downstream service / gateway / cloud API / partner API

Outbound HTTP adalah salah satu sumber failure paling umum:

  • timeout
  • connection leak
  • DNS issue
  • TLS issue
  • authentication failure
  • retry storm
  • slow downstream
  • response body tidak ditutup
  • wrong error mapping
  • large response buffering
  • partial read
  • stale connection
  • bad pool sizing

Karena itu, HTTP client harus diperlakukan sebagai production subsystem.


1. Core Mental Model

Inbound JAX-RS endpoint menerima request.

Outbound HTTP client membuat request ke dependency.

client request
  -> JAX-RS resource
  -> use case
  -> integration adapter
  -> HTTP client
  -> downstream service

Correct layering:

Resource method should not know HTTP client details.

Better:

Resource -> UseCase -> Port/Interface -> HTTP Adapter -> Jersey Client

This keeps:

  • API transport boundary separate from outbound integration
  • business logic testable
  • downstream error mapping centralized
  • resilience policy explicit
  • client lifecycle manageable

2. Jersey Client vs JAX-RS Client API

Important distinction:

JAX-RS Client API = standard API
Jersey Client = Jersey implementation of that API plus extensions

Standard packages often look like:

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;

Jersey-specific packages may look like:

import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.logging.LoggingFeature;

Do not assume Jersey Client is used just because server-side Jersey is used.

Some codebases use:

  • JAX-RS Client/Jersey Client
  • Java 11+ java.net.http.HttpClient
  • Apache HttpClient
  • OkHttp
  • Retrofit
  • OpenFeign
  • Spring WebClient
  • generated clients

Internal verification matters.


3. Client Lifecycle

Bad pattern:

public DownstreamResponse call() {
    Client client = ClientBuilder.newClient();
    return client.target(baseUrl)
            .path("/resource")
            .request()
            .get(DownstreamResponse.class);
}

Why bad?

  • creates client per call
  • loses connection pooling
  • expensive resource creation
  • harder to configure consistently
  • may leak resources if not closed

Better:

public final class DownstreamClient implements AutoCloseable {
    private final Client client;
    private final URI baseUri;

    public DownstreamClient(Client client, URI baseUri) {
        this.client = client;
        this.baseUri = baseUri;
    }

    public DownstreamResponse getResource(String id) {
        return client.target(baseUri)
                .path("/resources/{id}")
                .resolveTemplate("id", id)
                .request()
                .get(DownstreamResponse.class);
    }

    @Override
    public void close() {
        client.close();
    }
}

Client should usually be long-lived and closed during application shutdown.


4. Integration Adapter Pattern

Avoid leaking Jersey API into domain/use-case code.

Port:

public interface ProductCatalogGateway {
    ProductOffering getOffering(String offeringId);
}

Adapter:

public final class HttpProductCatalogGateway implements ProductCatalogGateway {
    private final Client client;
    private final URI baseUri;

    public ProductOffering getOffering(String offeringId) {
        Response response = client.target(baseUri)
                .path("/offerings/{id}")
                .resolveTemplate("id", offeringId)
                .request("application/json")
                .get();

        try {
            if (response.getStatus() == 404) {
                throw new ProductOfferingNotFound(offeringId);
            }

            if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
                throw DownstreamHttpException.from(response);
            }

            return response.readEntity(ProductOffering.class);
        } finally {
            response.close();
        }
    }
}

Use case sees domain-specific exception, not raw HTTP mechanics.


5. Response Closing Is Not Optional

When using Response, close it.

Response response = request.get();
try {
    return response.readEntity(MyDto.class);
} finally {
    response.close();
}

If response is not closed, connection may not return to pool.

Symptoms:

  • connection pool exhaustion
  • increasing latency
  • requests stuck waiting for connection
  • downstream appears slow but root cause is local leak

If using typed entity directly:

MyDto dto = request.get(MyDto.class);

client implementation handles response lifecycle differently, but you lose access to raw status/error body unless exception mapping is configured.

For senior code, explicit Response is often better when error handling matters.


6. WebTarget and Request Builder

JAX-RS Client builds requests fluently.

WebTarget target = client.target(baseUri)
        .path("/quotes/{quoteId}")
        .resolveTemplate("quoteId", quoteId)
        .queryParam("include", "pricing")
        .queryParam("tenant", tenantId);

Response response = target.request("application/json")
        .header("X-Correlation-ID", correlationId)
        .get();

Mental model:

Client -> WebTarget -> Invocation.Builder -> Response/entity

Avoid manual string concatenation for URLs.

Bad:

client.target(baseUrl + "/quotes/" + quoteId + "?include=pricing")

Risks:

  • encoding bug
  • path injection
  • double slash
  • query escaping issue
  • missing normalization

7. Timeout Configuration

Timeouts are mandatory.

There are usually multiple timeout types:

  • connect timeout
  • read/socket timeout
  • request timeout
  • connection acquisition timeout
  • DNS timeout, often indirect
  • TLS handshake timeout, often part of connect/socket behavior

Jersey-specific properties may be configured through ClientConfig or client properties, depending on version/runtime connector.

Conceptual example:

ClientConfig config = new ClientConfig();
config.property("jersey.config.client.connectTimeout", 1_000);
config.property("jersey.config.client.readTimeout", 3_000);

Client client = ClientBuilder.newClient(config);

Exact property constants and connector behavior must be verified for the Jersey version used internally.

Timeout rule:

Every outbound dependency must have an explicit timeout smaller than the inbound request budget.

8. Timeout Budget Hierarchy

If inbound request has 5 seconds total, downstream client cannot have 30 seconds read timeout.

Example budget:

Inbound gateway timeout:       10s
Service endpoint budget:        8s
DB budget:                      2s
Catalog HTTP call budget:       2s
Pricing HTTP call budget:       2s
Slack/notification call:        async/outbox, not in request path

Bad:

each downstream call timeout = 10s
three sequential calls
inbound gateway timeout = 10s

This guarantees timeout mismatch.

Timeouts must be designed as a budget tree.

More resilience patterns are covered in dedicated resilience parts; here the key is that HTTP client must expose timeout configuration clearly.


9. Connection Pooling

HTTP client performance often depends on connection reuse.

A production client should define:

  • max total connections
  • max connections per route/host
  • idle connection timeout
  • connection TTL
  • keep-alive strategy
  • connection acquisition timeout

The exact knobs depend on connector provider.

Jersey Client may use different connector providers, such as default connector, Apache connector, Jetty connector, or other integration depending on setup.

Internal verification is required.

Mental model:

thread wants outbound call
  -> acquire connection from pool
  -> send request
  -> read response
  -> close response
  -> connection returned to pool

If response is not closed, pool eventually drains.


10. Pool Sizing

Pool sizing should consider:

  • inbound concurrency
  • number of downstream hosts
  • average latency
  • p95/p99 latency
  • retry behavior
  • bulkhead limits
  • downstream capacity
  • pod replica count

Common mistake:

max connections per pod seems safe
but total connections = per pod * replicas * retry amplification

Example:

50 connections per pod
20 pods
= 1000 possible downstream connections

If downstream can handle only 200, your pool config is an attack vector.


11. Headers and Context Propagation

Outbound HTTP client should propagate approved context:

  • traceparent / tracestate
  • correlation id
  • causation id
  • tenant id if part of contract
  • idempotency key if needed
  • authorization token or service token
  • request id if platform requires it

Do not propagate arbitrary inbound headers blindly.

Risk:

  • header spoofing
  • leaking internal headers
  • tenant confusion
  • auth boundary bypass
  • high-cardinality observability explosion

Use an allowlist.


12. ClientRequestFilter

JAX-RS Client supports request filters.

Example:

public final class CorrelationClientFilter implements ClientRequestFilter {
    private final CorrelationContext correlationContext;

    public void filter(ClientRequestContext requestContext) {
        correlationContext.currentCorrelationId()
                .ifPresent(id -> requestContext.getHeaders().putSingle("X-Correlation-ID", id));
    }
}

Use filters for cross-cutting outbound concerns:

  • correlation header
  • trace propagation
  • standard user agent
  • tenant header
  • auth token injection
  • request logging metadata

Avoid filters that hide business logic.


13. ClientResponseFilter

Response filters can observe metadata:

public final class DownstreamTelemetryFilter implements ClientResponseFilter {
    public void filter(ClientRequestContext request, ClientResponseContext response) {
        int status = response.getStatus();
        String method = request.getMethod();
        URI uri = request.getUri();
        // record metric with low-cardinality route label, not full URI with IDs
    }
}

Be careful with body reading in response filters.

Reading entity stream in filter can consume body before caller reads it unless buffered/reset correctly.


14. Logging Outbound HTTP

Log enough to debug.

Do not log everything.

Useful fields:

  • downstream service name
  • method
  • route template
  • status
  • duration
  • timeout/failure category
  • correlation id
  • attempt number

Avoid:

  • full URL with sensitive query params
  • Authorization header
  • cookies
  • large request/response body
  • PII
  • tenant/customer identifiers as high-cardinality metric labels

For body logging, require explicit allowlist and environment policy.


15. Error Mapping

Do not leak downstream HTTP details to your API callers blindly.

Example downstream error:

GET product catalog -> 404

Could mean:

  • invalid user request
  • stale reference
  • downstream data lag
  • tenant mismatch
  • catalog service outage returning wrong error

Map it intentionally.

Adapter should translate:

HTTP status + error body + operation context

into domain/application errors:

  • CatalogItemNotFound
  • DownstreamUnavailable
  • DownstreamTimeout
  • UnauthorizedToCatalog
  • InvalidCatalogResponse

Then resource-level exception mapping decides API response.


16. Status Code Handling

Avoid treating all non-2xx as one exception.

Classify:

2xx -> success
3xx -> usually unexpected for service-to-service unless redirects allowed
400 -> caller/request issue or contract mismatch
401/403 -> auth/config/security issue
404 -> not found or wrong tenant/base URL
409 -> conflict/concurrency/domain state issue
422 -> semantic validation issue if downstream uses it
429 -> rate limited, retry policy required
5xx -> downstream/server issue

For production debugging, preserve downstream status in logs/metrics.

But external API response may need a different mapped error.


17. Reading Error Body Safely

Error bodies are useful but risky.

They may contain:

  • PII
  • stack traces
  • internal IDs
  • HTML error page
  • huge payload
  • non-JSON body

Safe pattern:

String safeErrorBody = readLimited(response, 8192);

Never assume error body is small or valid JSON.

Never log raw error body unless redaction policy allows it.


18. Entity Serialization

Outbound request body serialization uses entity providers.

Response response = client.target(baseUri)
        .path("/orders")
        .request("application/json")
        .post(Entity.json(requestDto));

Questions:

  • Which JSON provider is active?
  • Is it Jackson, JSON-B, or Jersey default?
  • How are Java time types serialized?
  • How are unknown fields handled?
  • Are nulls omitted or included?
  • Are enums serialized by name or custom value?

Client-side and server-side object mapper config can drift.


19. Large Request and Response Bodies

Do not read large responses into memory unless safe.

Bad:

byte[] bytes = response.readEntity(byte[].class);

Better for large content:

InputStream stream = response.readEntity(InputStream.class);
try (stream) {
    // stream to file/object storage/output stream
}

But when streaming, lifecycle becomes critical:

  • close stream
  • close response
  • handle partial read
  • handle checksum
  • handle timeout
  • avoid retry after partial side effect unless safe

Dedicated file/streaming client patterns are covered in the next integration parts.


20. TLS and Trust Configuration

Outbound HTTPS requires:

  • trust store
  • certificate chain validation
  • hostname verification
  • protocol/cipher policy
  • mTLS if required
  • certificate rotation

Anti-pattern:

disable hostname verification to make local/test work

This often becomes production risk.

If test environment uses internal CA, configure trust properly.


21. Authentication for Outbound Calls

Common outbound auth models:

  • bearer service token
  • OAuth2 client credentials
  • mTLS identity
  • signed request
  • API key
  • session/cookie, less common for service-to-service

HTTP client should not fetch token inefficiently per request unless token provider caches safely.

Better model:

ClientRequestFilter -> TokenProvider -> cached token -> Authorization header

TokenProvider must handle:

  • expiry
  • clock skew
  • refresh
  • failure
  • redaction
  • metrics

22. DNS and Service Discovery

Outbound URL may point to:

  • Kubernetes service DNS
  • API gateway
  • cloud private endpoint
  • partner endpoint
  • load balancer
  • service mesh virtual host

DNS failure modes:

  • stale DNS cache
  • wrong search domain
  • split-horizon DNS
  • private endpoint DNS misconfiguration
  • service name changed
  • environment config drift

Java DNS caching behavior can matter.

Verify runtime DNS TTL configuration when diagnosing endpoint move/failover issues.


23. Proxy and Gateway Behavior

Outbound call may traverse:

  • corporate proxy
  • service mesh sidecar
  • egress gateway
  • API gateway
  • cloud NAT
  • private endpoint

These layers can mutate behavior:

  • timeout
  • headers
  • TLS termination
  • retry
  • compression
  • max body size
  • connection reuse
  • DNS resolution

When debugging, draw actual network path.

Do not assume application directly connects to downstream pod/service.


24. Compression

Outbound client may send or receive compressed data.

Consider:

  • Accept-Encoding
  • transparent decompression
  • response size after decompression
  • zip bomb risk
  • CPU overhead
  • logging compressed/decoded sizes

Compression helps bandwidth but can hide memory and CPU cost.


25. Redirects

Service-to-service clients should usually not follow redirects blindly.

Redirects can cause:

  • auth token sent to unexpected host
  • method changed unexpectedly
  • POST body replay
  • hidden latency
  • routing misconfiguration masked

If redirects are allowed, define policy explicitly.


26. Generated Clients

Some organizations generate HTTP clients from OpenAPI.

Benefits:

  • contract consistency
  • less handwritten mapping
  • typed request/response
  • easier consumer onboarding

Risks:

  • generated code hides lifecycle
  • bad default timeouts
  • poor error mapping
  • dependency sprawl
  • difficult customization
  • drift between generated client and runtime config

Generated client still needs production wrapper.

GeneratedApiClient -> InternalGatewayAdapter -> UseCase

27. Testing HTTP Clients

Test levels:

  1. Unit test adapter mapping.
  2. Mock HTTP server test.
  3. Contract test against OpenAPI expectations.
  4. Integration test with real downstream sandbox if available.
  5. Fault injection for timeout, 500, invalid JSON, slow body, partial response.

Do not only test happy path.

Important cases:

  • 404 mapping
  • 401/403 mapping
  • 429 handling
  • 5xx handling
  • timeout
  • invalid JSON
  • missing required field
  • large body
  • connection refused

28. Observability for Outbound HTTP

Metrics:

  • request count by downstream and route template
  • latency histogram
  • status family
  • timeout count
  • connection pool usage
  • retry count, if retries are in this layer
  • circuit breaker state, if used

Trace:

inbound span
  -> application span
  -> outbound HTTP client span
      attributes: method, route, status, downstream service

Logs:

  • downstream name
  • operation
  • status/failure
  • duration
  • correlation id
  • safe error code

Avoid full URL with raw IDs as metric label.


29. Failure Mode: Connection Pool Exhaustion

Symptoms:

  • threads waiting before sending request
  • latency rises even when downstream is healthy
  • pool acquisition timeout
  • connection count at max

Causes:

  • response not closed
  • pool too small
  • downstream slow
  • retry storm
  • missing bulkhead
  • large streaming responses held too long

Debug path:

check pool metrics
check response close patterns
check thread dumps
check downstream latency
check retry/circuit breaker state
check recent traffic increase

30. Failure Mode: Timeout Mismatch

Symptoms:

  • gateway returns 504
  • application logs downstream timeout after client disconnected
  • wasted work continues after caller gone

Causes:

  • outbound timeout greater than inbound timeout
  • missing cancellation
  • sequential calls exceed budget
  • retry policy ignores remaining time

Mitigation:

  • budget propagation
  • per-operation timeout
  • deadline-aware retry
  • fail fast
  • async/outbox for non-critical side effect

31. Failure Mode: Wrong Error Mapping

Symptoms:

  • downstream 404 becomes public 500
  • downstream 401 becomes user-facing validation error
  • retry on non-retryable 400
  • no alert on downstream 5xx

Causes:

  • generic catch-all exception
  • lack of downstream error taxonomy
  • typed client hides response
  • missing integration test

Mitigation:

  • explicit mapping table
  • preserve downstream details in internal logs
  • expose stable API error to callers
  • test every important status class

32. Failure Mode: Invalid Response Contract

Symptoms:

  • JSON parse error
  • missing field
  • enum unknown
  • date parse failure
  • null unexpectedly appears

Causes:

  • downstream breaking change
  • object mapper mismatch
  • generated client outdated
  • no contract test
  • incompatible enum evolution

Mitigation:

  • OpenAPI compatibility checks
  • tolerant readers when appropriate
  • generated client regeneration pipeline
  • consumer-driven contract tests
  • strict monitoring after deployment

33. Failure Mode: Header Leakage or Spoofing

Symptoms:

  • wrong tenant context downstream
  • user token forwarded to service that should use service identity
  • internal header influences authorization
  • PII appears in logs

Causes:

  • blindly forwarding inbound headers
  • no header allowlist
  • weak trust boundary
  • inconsistent auth propagation

Mitigation:

  • header allowlist
  • service identity separation
  • tenant resolver validation
  • log redaction
  • security tests

34. Basic Jersey Client Factory Shape

Example conceptual factory:

public final class JerseyClientFactory {
    public Client create(ClientSettings settings, List<Object> providers) {
        ClientConfig config = new ClientConfig();

        config.property("jersey.config.client.connectTimeout", settings.connectTimeoutMillis());
        config.property("jersey.config.client.readTimeout", settings.readTimeoutMillis());

        for (Object provider : providers) {
            config.register(provider);
        }

        return ClientBuilder.newBuilder()
                .withConfig(config)
                .build();
    }
}

In real code, property names, connector provider, TLS config, JSON provider, and pool config must match internal library versions.


35. Adapter Error Mapping Example

private RuntimeException mapError(Response response, String operation) {
    int status = response.getStatus();
    String body = safeReadErrorBody(response);

    if (status == 404) {
        return new DownstreamNotFound(operation);
    }

    if (status == 401 || status == 403) {
        return new DownstreamAuthorizationFailure(operation);
    }

    if (status == 429) {
        return new DownstreamRateLimited(operation);
    }

    if (status >= 500) {
        return new DownstreamUnavailable(operation, status);
    }

    return new DownstreamUnexpectedResponse(operation, status);
}

Important:

safeReadErrorBody must not consume unlimited memory or log sensitive data.

36. PR Review Checklist

Review lifecycle:

  • Client is not created per request.
  • Client is closed on application shutdown.
  • Connection pooling is configured or explicitly unnecessary.
  • Response is closed when using raw Response.

Review timeout:

  • Connect/read/request timeouts are explicit.
  • Timeout fits inbound budget.
  • Retry policy, if any, respects timeout budget.

Review error mapping:

  • Important status codes are handled intentionally.
  • Downstream error does not leak raw details externally.
  • Internal logs preserve enough diagnostic context.

Review context/security:

  • Correlation/tracing context is propagated.
  • Tenant context is propagated only if contract requires it.
  • Headers are allowlisted.
  • Authorization model is explicit.

Review observability:

  • Metrics use low-cardinality labels.
  • Logs include downstream operation and failure category.
  • Traces include outbound span.

Review testability:

  • Use case depends on interface, not raw HTTP client.
  • Adapter has mock server tests.
  • Error cases are tested.

37. Internal Verification Checklist

Do not assume. Verify:

  • Which HTTP client library is used internally?
  • Is Jersey Client used, or another client?
  • Which Jersey version is used?
  • Which connector provider is configured?
  • How are connection pools configured?
  • Where are timeout defaults defined?
  • Is there a platform-standard HTTP client wrapper?
  • Is Resilience4j or equivalent used around HTTP calls?
  • Is OpenTelemetry auto-instrumentation enabled?
  • Are client request/response filters registered?
  • How is correlation ID propagated?
  • How is tenant context propagated?
  • How are auth tokens/service credentials attached?
  • Is mTLS required for service-to-service calls?
  • Is proxy/service mesh/egress gateway involved?
  • Are generated clients used from OpenAPI?
  • What is the standard error mapping convention?
  • Are error bodies redacted?
  • Are HTTP client metrics available?
  • Are pool metrics available?
  • Are timeout/retry/circuit breaker configs visible in runtime config?

38. Senior Mental Model

Outbound HTTP is not “just call another API”.

It is a dependency boundary with:

  • contract risk
  • latency risk
  • security risk
  • resource risk
  • observability risk
  • retry amplification risk
  • tenant/context propagation risk

A senior engineer asks:

What is the downstream contract?
What is the timeout budget?
What happens if the downstream is slow?
What happens if the response body is never closed?
What identity is used?
What headers are allowed?
How is error mapped?
How is this observed?
Can this call be moved out of the synchronous request path?

39. Summary

In this part, you learned:

  • how outbound HTTP fits inside JAX-RS service architecture
  • how to separate standard JAX-RS Client API from Jersey Client specifics
  • why client lifecycle and response closing matter
  • how connection pooling and timeout budget shape production behavior
  • how to propagate context safely
  • how to map downstream errors intentionally
  • how to review HTTP client code as a senior engineer

The key invariant:

Every outbound HTTP call must have explicit lifecycle, timeout, error mapping, security context, and observability.

Next part compares declarative HTTP clients such as Retrofit and OpenFeign.

Lesson Recap

You just completed lesson 80 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.