| <?xml version="1.0" encoding="UTF-8"?> | |
| <!-- | |
| ~ Licensed to the Apache Software Foundation (ASF) under one | |
| ~ or more contributor license agreements. See the NOTICE file | |
| ~ distributed with this work for additional information | |
| ~ regarding copyright ownership. The ASF licenses this file | |
| ~ to you under the Apache License, Version 2.0 (the | |
| ~ "License"); you may not use this file except in compliance | |
| ~ with the License. You may obtain a copy of the License at | |
| ~ | |
| ~ http://www.apache.org/licenses/LICENSE-2.0 | |
| ~ | |
| ~ Unless required by applicable law or agreed to in writing, | |
| ~ software distributed under the License is distributed on an | |
| ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| ~ KIND, either express or implied. See the License for the | |
| ~ specific language governing permissions and limitations | |
| ~ under the License. | |
| --> | |
| <document xmlns="http://maven.apache.org/XDOC/2.0" | |
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd"> | |
| <properties> | |
| <title>HTTP/2 Java Client — Sample Code</title> | |
| </properties> | |
| <body> | |
| <section name="HTTP/2 Java Client — Sample Code"> | |
| <p><strong>What this is:</strong> A standalone sample client | |
| (<code>Http2JsonClient</code>) that demonstrates how to call Axis2 | |
| JSON-RPC services over HTTP/2 from plain Java using Apache HttpClient 5. | |
| It is <em>not</em> part of the Axis2 framework — it is example code | |
| in the userguide samples that you can copy and adapt for your own | |
| project.</p> | |
| <p><strong>Why it exists:</strong> Java's built-in | |
| <code>HttpURLConnection</code> does not support HTTP/2. Apache | |
| HttpClient 5's convenience classes (<code>SimpleHttpRequest</code> / | |
| <code>SimpleHttpResponse</code>) support HTTP/2 but silently buffer | |
| the entire response in memory, defeating the streaming benefit. | |
| This sample shows the correct pattern — using | |
| <code>AbstractBinResponseConsumer</code> with the async API — so | |
| you don't have to rediscover it the hard way.</p> | |
| <p>Two execution modes:</p> | |
| <ul> | |
| <li><strong>Buffered</strong> — returns the full response as a | |
| <code>String</code>. Simple, suitable for responses that fit | |
| in memory.</li> | |
| <li><strong>Streaming</strong> — writes response bytes to an | |
| <code>OutputStream</code> in 64KB chunks as HTTP/2 DATA | |
| frames arrive. Memory stays flat regardless of response size. | |
| When paired with the | |
| <a href="json-streaming-formatter.html">Streaming JSON | |
| Formatter</a> (AXIS2-6103), data flows end-to-end in 64KB | |
| chunks.</li> | |
| </ul> | |
| </section> | |
| <section name="The SimpleHttp* Pitfall"> | |
| <p>Apache HttpClient 5 provides <code>SimpleHttpRequest</code> and | |
| <code>SimpleHttpResponse</code> as convenience classes for async | |
| requests. <strong>Do not use them for HTTP/2 workloads with large | |
| responses.</strong> They appear to work, but they silently defeat | |
| HTTP/2 streaming.</p> | |
| <p><code>SimpleHttpResponse</code> is a buffering response object — | |
| it accumulates the entire response body in memory before returning | |
| it to the caller. For a 100MB response:</p> | |
| <ul> | |
| <li><code>SimpleHttpResponse</code>: allocates 100MB+ of heap | |
| (internal byte arrays, header maps, content type parsing) | |
| before your code sees a single byte</li> | |
| <li><code>AbstractBinResponseConsumer</code>: your | |
| <code>data(ByteBuffer)</code> callback fires for each 64KB | |
| HTTP/2 DATA frame — memory stays flat at ~64KB working | |
| set</li> | |
| </ul> | |
| <p>This is not obvious from the HttpClient 5 documentation, and | |
| it is easy to write code that uses <code>SimpleHttpResponse</code>, | |
| observes correct HTTP/2 ALPN negotiation in the logs, and concludes | |
| that HTTP/2 streaming is working — when in fact the response is | |
| fully buffered before your code runs. The sample client avoids this | |
| by using <code>AbstractBinResponseConsumer</code> for all requests, | |
| including the buffered convenience method.</p> | |
| </section> | |
| <section name="Dependencies"> | |
| <p>Requires Java 11+ (ALPN built in) and Apache HttpClient 5.4+:</p> | |
| <source> | |
| <!-- Maven --> | |
| <dependency> | |
| <groupId>org.apache.httpcomponents.client5</groupId> | |
| <artifactId>httpclient5</artifactId> | |
| <version>5.4.3</version> | |
| </dependency> | |
| <dependency> | |
| <groupId>org.apache.httpcomponents.core5</groupId> | |
| <artifactId>httpcore5-h2</artifactId> | |
| <version>5.4.3</version> | |
| </dependency> | |
| </source> | |
| <p>These are the same dependencies used by Axis2's own | |
| <code>H2TransportSender</code>. If you are already running Axis2 | |
| with HTTP/2 transport, they are already on your classpath.</p> | |
| <p>The per-probe TCP-keepalive settings shown under Production | |
| Hardening (<code>setTcpKeepIdle/Interval/Count</code>) require | |
| httpcore5 5.3 or later.</p> | |
| </section> | |
| <section name="Buffered Execution"> | |
| <p>POST JSON-RPC to any Axis2 service, get the response as a String:</p> | |
| <source> | |
| String url = "https://localhost:8443/axis2-json-api/services/FinancialBenchmarkService"; | |
| String json = "{\"monteCarlo\":[{\"arg0\":{\"nSimulations\":100000,\"nPeriods\":252," | |
| + "\"initialValue\":1000000,\"expectedReturn\":0.10,\"volatility\":0.223," | |
| + "\"nPeriodsPerYear\":252,\"randomSeed\":42}}]}"; | |
| String response = Http2JsonClient.execute(url, json, 300); | |
| System.out.println(response); | |
| Http2JsonClient.shutdown(); | |
| </source> | |
| <p>The client negotiates HTTP/2 via ALPN on the TLS handshake. | |
| Connections are pooled and multiplexed — multiple concurrent requests | |
| share a single TCP connection.</p> | |
| </section> | |
| <section name="Streaming Execution"> | |
| <p>For large responses (10MB+), stream to a file or parser instead | |
| of buffering in heap:</p> | |
| <source> | |
| String url = "https://localhost:8443/axis2-json-api/services/BigDataH2Service"; | |
| String json = "{\"generate\":[{\"arg0\":{\"datasetSize\":52428800}}]}"; | |
| try (FileOutputStream fos = new FileOutputStream("/tmp/result.json")) { | |
| int status = Http2JsonClient.executeStreaming(url, json, 300, fos); | |
| System.out.println("HTTP " + status); | |
| } | |
| Http2JsonClient.shutdown(); | |
| </source> | |
| <p>Each HTTP/2 DATA frame triggers a callback that writes directly | |
| to your <code>OutputStream</code>. The <code>capacityIncrement()</code> | |
| returns 64KB, creating natural HTTP/2 flow control backpressure — | |
| the client tells the server "I can accept 64KB more" after each | |
| chunk.</p> | |
| <p>When the server uses the | |
| <a href="json-streaming-formatter.html">Streaming JSON Formatter</a>, | |
| data flows end-to-end without full-body buffering on either side:</p> | |
| <source> | |
| Server (MoshiStreamingMessageFormatter) | |
| → FlushingOutputStream flushes every 64KB | |
| → HTTP/2 DATA frames | |
| → Http2JsonClient.data() callback | |
| → your OutputStream | |
| </source> | |
| </section> | |
| <section name="Production Hardening"> | |
| <p>The examples above establish HTTP/2 connectivity, but a long-lived | |
| pooled client that runs for days against a load-balanced service needs | |
| more than that to stay healthy across upstream restarts and network | |
| blips. Learned in production: without the four defenses below, a routine | |
| upstream restart or a briefly dropped network path leaves the client | |
| dispatching requests onto dead connections that hang until the socket | |
| timeout. Each defense is a few lines on the client builder, and the | |
| sample applies all four.</p> | |
| <subsection name="1. Stale-connection defenses"> | |
| <p>A connection pool will hand out a connection the server (or an | |
| intermediary) has since closed. After an upstream restart the pool keeps | |
| dispatching onto dead, half-open connections; each request hangs until the | |
| socket timeout. Three settings prevent this:</p> | |
| <source> | |
| ConnectionConfig connectionConfig = ConnectionConfig.custom() | |
| .setConnectTimeout(Timeout.ofSeconds(30)) | |
| .setValidateAfterInactivity(TimeValue.ofSeconds(10)) // re-check idle connections on lease | |
| .setTimeToLive(TimeValue.ofMinutes(5)) // cap connection lifetime | |
| .build(); | |
| // ...setDefaultConnectionConfig(connectionConfig) on the connection manager | |
| HttpAsyncClients.custom() | |
| // ... | |
| .evictExpiredConnections() | |
| .evictIdleConnections(TimeValue.ofSeconds(90)) // background drain of idle connections | |
| .build(); | |
| </source> | |
| <p><code>validateAfterInactivity</code> revalidates a pooled connection | |
| before it is leased if it has been idle beyond the threshold; | |
| <code>timeToLive</code> retires connections regardless of traffic; the | |
| background evictor drains idle/expired connections so a restart-killed | |
| connection dies proactively rather than being discovered on the next | |
| request. All three act only on pooled/idle connections — an in-flight | |
| request is never cut.</p> | |
| </subsection> | |
| <subsection name="2. TCP keepalive"> | |
| <p>The defenses above catch a connection the peer closed cleanly (FIN/RST). | |
| They do not catch a <em>silently</em> dead peer — a hard kill, or an idle | |
| TCP flow dropped by a NAT gateway or load balancer without notifying either | |
| end. Only TCP keepalive detects that:</p> | |
| <source> | |
| IOReactorConfig.custom() | |
| .setSoKeepAlive(true) | |
| .setTcpKeepIdle(45) // seconds idle before the first probe | |
| .setTcpKeepInterval(10) // seconds between probes | |
| .setTcpKeepCount(3) // probes before declaring the peer dead | |
| .build(); | |
| </source> | |
| <p>With these, a dead peer is detected in roughly 75-105s instead of the | |
| OS keepalive default (commonly two hours). Two caveats:</p> | |
| <ul> | |
| <li>The per-probe knobs (<code>TcpKeepIdle/Interval/Count</code>) use | |
| <code>jdk.net.ExtendedSocketOptions</code> and require Java 11+ on | |
| Linux or macOS. On other platforms <code>setSoKeepAlive(true)</code> | |
| still works but falls back to the OS default idle time.</li> | |
| <li>Keep the idle time <strong>below the shortest idle-drop on the | |
| network path</strong>. NAT gateways commonly reset idle flows at ~350s; | |
| cloud load-balancer idle timeouts vary and can be as low as 60s. A | |
| keepalive idle longer than the path's timeout defeats the purpose — the | |
| flow is already gone before the first probe.</li> | |
| </ul> | |
| </subsection> | |
| <subsection name="3. Retry on stale-connection failures"> | |
| <p>Even with validation there is a race: a connection can die between the | |
| lease-time check and the request being written. It then fails with a | |
| recognizable connection-level exception rather than a response, and | |
| retrying once on a fresh connection recovers transparently:</p> | |
| <source> | |
| if (cause instanceof ConnectionClosedException // GOAWAY race / not-executed | |
| || cause instanceof HttpStreamResetException // stream reset (incl. H2) | |
| || cause instanceof H2ConnectionException | |
| || cause instanceof ClosedChannelException | |
| || cause instanceof ConnectionRequestTimeoutException | |
| || cause instanceof DeadlineTimeoutException // pool-lease timeout | |
| || cause instanceof SocketException | |
| || cause instanceof ConnectException | |
| || cause instanceof SocketTimeoutException) { | |
| // transient — safe to retry on a fresh, validated connection | |
| } | |
| </source> | |
| <p>Two rules keep retry safe:</p> | |
| <ul> | |
| <li><strong>Idempotency</strong> — only retry operations that can run | |
| twice without side effects. A read/query is fine; a non-idempotent | |
| write is not.</li> | |
| <li><strong>No partial output</strong> — when streaming to a caller's | |
| <code>OutputStream</code>, retry only while zero bytes have been | |
| written; once any body byte has flowed the stream cannot be rewound, so | |
| the failure must surface. (The buffered <code>execute</code> path uses a | |
| fresh buffer per attempt, so it is always safe.)</li> | |
| </ul> | |
| <p>A first-attempt <code>Future.get()</code> <code>TimeoutException</code> is | |
| also worth retrying once: it is the common signature of a request dispatched | |
| onto a dead pooled connection, and retrying on a validated connection avoids | |
| a full-timeout hang without tripling the worst-case latency of a genuinely | |
| slow request.</p> | |
| </subsection> | |
| <subsection name="4. Fail fast on the response status"> | |
| <p>This is why the sample uses <code>AbstractBinResponseConsumer</code> | |
| rather than <code>SimpleHttpResponse</code> for <em>every</em> request, | |
| including the buffered one. The consumer's <code>start()</code> callback | |
| receives the status code the moment response headers arrive — before any | |
| body. A failing intermediary (for example a load balancer whose target died | |
| mid-request) can return error headers and then never terminate the body | |
| stream; code that waits for the full body before checking the status parks | |
| for the entire timeout. Reacting to the status in <code>start()</code> turns | |
| that multi-minute hang into an immediate, retryable failure.</p> | |
| </subsection> | |
| </section> | |
| <section name="Timeout and Cancellation"> | |
| <p>Both methods accept a <code>timeoutSeconds</code> parameter for | |
| <code>Future.get()</code>. If the timeout expires or the thread is | |
| interrupted, the underlying HTTP request is cancelled to prevent | |
| zombie requests that would continue consuming resources:</p> | |
| <source> | |
| try { | |
| response = future.get(timeoutSeconds, TimeUnit.SECONDS); | |
| } catch (Exception e) { | |
| requestFuture.cancel(true); // Cancel the HTTP request | |
| if (e instanceof InterruptedException) { | |
| Thread.currentThread().interrupt(); // Restore interrupt flag | |
| } | |
| throw e; | |
| } | |
| </source> | |
| </section> | |
| <section name="Source Code"> | |
| <p>The complete sample client is available on GitHub:</p> | |
| <p><a href="https://github.com/apache/axis-axis2-java-core/blob/master/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java"> | |
| Http2JsonClient.java on GitHub</a></p> | |
| <p>Copy and adapt it for your project. It has no dependency on | |
| Axis2 itself — only Apache HttpClient 5 and httpcore5-h2.</p> | |
| </section> | |
| </body> | |
| </document> |