DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Two AI Agents, One Database: Solving Concurrent Updates With Optimistic Locking
  • A Non-Expiring AKS Deployment Token Could Read Secrets Across Three Namespaces
  • Share Delta Lake Tables Between Databricks and Snowflake
  • Designing Memory Architecture for Stateful AI Agents: Short-Term Context and Long-Term Persistence

Trending

  • Prompt Caching for LLMs, Part 2: Semantic Caching in Production
  • Claude Code Superpowers: The 5-Tool Stack That Ends Limits and Context Amnesia
  • Slopsquatting: Building a Scanner That Catches AI-Hallucinated Packages Before They Reach Production
  • Building a Real Pipeline With senro: Monorepos, Caching, and Triggers

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  1. DZone
  2. Data Engineering
  3. Data
  4. Persisting Cookies Across JAX-WS Service Calls Using a Custom SOAP Handler

Persisting Cookies Across JAX-WS Service Calls Using a Custom SOAP Handler

A custom SOAP handler preserves JAX-WS session cookies across calls without JVM-wide state, using ThreadLocal isolation and explicit cleanup to prevent session leaks.

By 
Vishal Rameshchandra Shah
·
Nov. 12, 26 · Tutorial
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
152 Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

The Cookie That Never Came Back

The first call authenticated the user. The second failed.

I was integrating with a customer-provided SOAP service exposed through a single WSDL, with JAX-WS client classes generated by IBM Rational Application Developer from the WSDL and its XSDs. The first call authenticated the user and came back with a Set-Cookie header. Every call after it needed that cookie. The generated client sent none of it. Each invocation behaved as an independent conversation, and the workflow broke on its second step. No exception, no warning. Just a server that had forgotten who you are.

The Cookie That Never Came Back

The Built-in Answer, and Why It Failed

The JAX-WS specification anticipated this: every proxy generated from a WSDL implements BindingProvider, and the spec defines a request-context property for exactly this purpose:

Java
 
CheckPort port = new Check().getCheckPort();
((BindingProvider) port).getRequestContext()
    .put(BindingProvider.SESSION_MAINTAIN_PROPERTY, Boolean.TRUE);


Set that flag, and the runtime is supposed to maintain session state. In most tutorials, this is where the article ends. Mine broke in deployment for three reasons.

First is scope. The property is set on one proxy instance, and the runtime's cookie store lives with that proxy. RAD-generated code encourages pulling a fresh port per unit of work. Every fresh port gets a fresh store, so the authentication session is discarded when the method returns. Holding one proxy for the workflow fixes it, but pins the session lifetime to the object lifetime.

Second is opacity. The spec does not say where the runtime keeps the cookie. The JDK's bundled implementation delegates to java.net.CookieHandler.getDefault(), null unless somebody installed one. IBM's runtime, which RAD-generated clients typically run on under WebSphere, does it its own way. Same client class, different behavior per container, and the failure is silent.

Third is the obvious fix: install a default CookieManager inline.

`CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));`

A JVM-wide static. Fine in a standalone client, but inside an application server it is shared by every deployed application and every outbound HTTP client in the JVM. It is also the wrong model for a server-side client: a CookieManager is built for a browser, where one cookie jar belongs to one user. A server handling concurrent requests would pool every user's session cookies into one store, and CookieManager applies RFC domain and path matching, so if the service sits behind a load balancer, the cookie is quietly dropped.

The Options on the Table

I wanted cookie handling scoped to the call, visible in the code, and independent of the container. The alternatives fell short. Holding one proxy for the whole workflow is the simplest change, but it marries the session to an object's lifetime, and the strain shows across components or threads. Installing a default CookieManager is one line and works in a standalone client, but inside an app server it becomes shared mutable state across applications and users, which rules it out for anything concurrent. Hand-rolling the client on plain HTTP gives full header control with no container surprises, but you lose the WSDL tooling: generated types, binding, error handling. Too much machinery to rebuild for several operations.

A custom SOAP handler was the fourth option: roughly sixty lines you own, sitting in the message path of the existing generated client. Nothing JVM-wide, nothing container-specific, nothing hand-edited in generated classes. The costs: you match the cookie by name instead of trusting RFC rules, and you own the stored value's lifecycle. Acceptable for this integration.

How the Handler Works

The contract is SOAPHandler<SOAPMessageContext>; the direction comes from a boolean on the context. The listing below is illustrative, adapted from the class I used, and not presented as tested code.

Java
 
 public boolean handleMessage(SOAPMessageContext ctx) {
        if (Boolean.TRUE.equals(ctx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY))) {
            attachCookie(ctx);
        } else {
            captureCookie(ctx);
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    private void captureCookie(SOAPMessageContext ctx) {
        Map<String, List<String>> headers =
            (Map<String, List<String>>) ctx.get(MessageContext.HTTP_RESPONSE_HEADERS);
        if (headers == null) {
            return;
        }
        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
            if (!"Set-Cookie".equalsIgnoreCase(entry.getKey())) {
                continue;
            }
            for (String raw : entry.getValue()) {
                String pair = raw.split(";", 2)[0].trim();
                if (pair.startsWith("JSESSIONID=")) {
                    SESSION_COOKIE.set(pair);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void attachCookie(SOAPMessageContext ctx) {
        String cookie = SESSION_COOKIE.get();
        if (cookie == null) {
            return;
        }
        Map<String, List<String>> existing =
            (Map<String, List<String>>) ctx.get(MessageContext.HTTP_REQUEST_HEADERS);
        Map<String, List<String>> headers =
            (existing == null) ? new HashMap<>() : new HashMap<>(existing);
        headers.put("Cookie", Collections.singletonList(cookie));
        ctx.put(MessageContext.HTTP_REQUEST_HEADERS, headers);
    }

    public boolean handleFault(SOAPMessageContext ctx) {
        return true;
    }

    public void close(MessageContext ctx) {
    }

    public Set<QName> getHeaders() {
        return Collections.emptySet();
    }
}

Three details in that listing exist because earlier versions broke. Copying the existing request headers before adding Cookie keeps the handler additive: putting a fresh map into HTTP_REQUEST_HEADERS replaces whatever was already there, and my first version did exactly that, until another handler down the chain set a custom authorization header that then vanished. 

Matching the header name case-insensitively is for portability, since a case-insensitive map is an implementation choice, not a spec guarantee. Splitting on the first semicolon keeps response attributes such as Path, Expires, and HttpOnly out of the request; echoing them back produces a Cookie value the server will not parse. Iterating over every value rather than taking index zero covers responses that set more than one cookie, where the session cookie is not guaranteed to be first.

Why Threadlocal, and Where It Bites

The integration ran several workflows concurrently, so a static field would leak one user's session into another's request. A ThreadLocal gives each thread its own value, which fits a workflow that runs start to finish on one thread.

The cost is that thread pools reuse threads. If a workflow ends without clearing the value, the next request on that thread inherits a stale session cookie, and in the worst case sends one user's session identifier on another user's behalf. Clearing it when the workflow ends closes that hole:

SESSION_COOKIE.remove(); 

Note that close() is intentionally empty: the container calls it at the end of each message exchange, not the workflow, so clearing there would discard the cookie from the authentication call. Cleanup belongs at the logout step or in a finally block around the workflow. And if the workflow hands off between threads, ThreadLocal is the wrong container: the cookie then belongs in whatever object already represents the conversation.

Registering the handler

JAX-WS lets handlers be declared in an external XML file:

XML
 
<?xml version="1.0" encoding="UTF-8"?>
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
    <handler-chain>
        <handler>
            <handler-class>com.company.cc.handler.SessionCookieHandler</handler-class>
        </handler>
    </handler-chain>
</handler-chains>

The file sits in the same package as the generated service class, since the annotation resolves relative to that class:

Java
 
@WebServiceClient(
name = "check",
targetNamespace = "http://ws.example.com/",
wsdlLocation = "META-INF/wsdl/spellingCheck.wsdl"
)
@HandlerChain(file = "sessionCookieHandler.xml")
public class Check extends Service {
}


Every proxy from that service now runs through the handler. One caveat: regenerating from an updated WSDL overwrites the service class and removes the annotation, so keep it in a small subclass or script the regeneration step.

Edge Cases

Cookie rotation needs no special handling: capture runs on every inbound response, so a new cookie mid-workflow replaces the old value on the next exchange. Expiry is different: a ThreadLocal cannot know when the server-side session times out, so an idle workflow keeps attaching a dead cookie. That fix belongs at the workflow level: catch the session-expired response and re-authenticate.

Testing It without a Container

A test can build a stub message context, inject HTTP_RESPONSE_HEADERS, run handleMessage, and check the Cookie header on the next outbound call. Cover the cases that burned earlier versions: two Set-Cookie values in one response, a header name in unexpected case, a pre-existing request map that must survive the copy, and the remove() at the end of the workflow.

For integration confidence, run the real client against a stub endpoint that returns Set-Cookie on the first call and asserts the Cookie header on the second.

The Takeaway

JAX-WS has a session mechanism. It is scoped to a proxy instance, implemented differently across runtimes, and backed by a JVM-wide singleton that does not fit a server-side client handling concurrent users. A custom SOAP handler replaces it with a small class you own, an explicit scope you choose, and behavior identical wherever the application deploys.

For teams maintaining legacy SOAP integrations that will not be modernized soon, that trade is usually the right one.

SOAP Data Types

Opinions expressed by DZone contributors are their own.

Related

  • Two AI Agents, One Database: Solving Concurrent Updates With Optimistic Locking
  • A Non-Expiring AKS Deployment Token Could Read Secrets Across Three Namespaces
  • Share Delta Lake Tables Between Databricks and Snowflake
  • Designing Memory Architecture for Stateful AI Agents: Short-Term Context and Long-Term Persistence

Partner Resources

×

Comments

Loading comments

The comments didn't load

Please try again in a moment.

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook