You are viewing a plain text version of this content. The canonical link for it is here.
Posted to c-commits@axis.apache.org by ro...@apache.org on 2026/09/25 12:51:12 UTC
(axis-axis2-c-core) branch master updated (cfb435fa5 -> a0c52b3b0)
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
from cfb435fa5 Read n_periods_per_year the same strict way in every operation
new b98ad3316 Add an HTTP/2 JSON client, libaxis2_h2_client
new 95fff174a Reopen an HTTP/2 connection the server closed while it was idle
new cac64816b Bound the h2 client's handshake writes by connect_timeout_ms
new 46d843d1d Keep a complete h2 response when the server closes right after it
new ca03d215d Bound a whole h2 client post with request_timeout_ms
new a0c52b3b0 Note that a long-running h2 client call needs both timeouts raised
The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
docs/HTTP2_JSON_CLIENT.md | 100 ++
include/axis2_h2_json_client.h | 159 +++
src/core/transport/h2/sender/Makefile.am | 17 +-
.../transport/h2/sender/axis2_h2_json_client.c | 1133 ++++++++++++++++++++
src/core/transport/h2/test/Makefile.am | 16 +-
src/core/transport/h2/test/h2_json_client_test.c | 239 +++++
6 files changed, 1660 insertions(+), 4 deletions(-)
create mode 100644 docs/HTTP2_JSON_CLIENT.md
create mode 100644 include/axis2_h2_json_client.h
create mode 100644 src/core/transport/h2/sender/axis2_h2_json_client.c
create mode 100644 src/core/transport/h2/test/h2_json_client_test.c
(axis-axis2-c-core) 04/06: Keep a complete h2 response when the server closes right after it
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 46d843d1d9b159497a9cfcfaa9a0a798953e8e66
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Thu Sep 24 14:12:28 2026 -1000
Keep a complete h2 response when the server closes right after it
A close_notify read in the same batch as the end of the response, or a
failed send of the trailing ACKs, discarded a response that had fully
arrived. Both now return it and drop the connection. content-length is
formatted as unsigned long long, which holds any size_t.
Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
src/core/transport/h2/sender/axis2_h2_json_client.c | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/src/core/transport/h2/sender/axis2_h2_json_client.c b/src/core/transport/h2/sender/axis2_h2_json_client.c
index 535f812ab..c330a560d 100644
--- a/src/core/transport/h2/sender/axis2_h2_json_client.c
+++ b/src/core/transport/h2/sender/axis2_h2_json_client.c
@@ -470,6 +470,12 @@ h2c_pump_recv(axis2_h2_json_client_t *c, const axutil_env_t *env)
return AXIS2_FAILURE;
break;
case SSL_ERROR_ZERO_RETURN:
+ /* close_notify can arrive in the same read as the end of the
+ * response. Let the caller see what was decoded first; if
+ * the stream is still open, the next call gets here again
+ * with got_any 0 and fails (OpenSSL repeats ZERO_RETURN). */
+ if (got_any)
+ return AXIS2_SUCCESS;
h2c_fail(c, env, "the server closed the TLS session");
return AXIS2_FAILURE;
default:
@@ -948,7 +954,7 @@ axis2_h2_json_client_post(
c->resp_cap = 0;
c->resp_too_large = 0;
- snprintf(clen, sizeof(clen), "%lu", (unsigned long)json_len);
+ snprintf(clen, sizeof(clen), "%llu", (unsigned long long)json_len);
#define H2C_NV(i, n, v) do { \
hdrs[i].name = (uint8_t *)(n); hdrs[i].namelen = strlen(n); \
hdrs[i].value = (uint8_t *)(v); hdrs[i].valuelen = strlen(v); \
@@ -1000,9 +1006,15 @@ axis2_h2_json_client_post(
if (h2c_pump_recv(c, env) != AXIS2_SUCCESS)
goto fail;
}
- /* Acknowledge what arrived with the response (settings, window updates). */
+ /* Acknowledge what arrived with the response (settings, window updates).
+ * Best effort: the response is complete, and a server that closed the
+ * connection right after sending it makes this send fail. Drop the
+ * connection then, but do not discard the response. */
if (h2c_pump_send(c, env) != AXIS2_SUCCESS)
- goto fail;
+ {
+ h2c_disconnect(c, env, 0);
+ c->error[0] = '\0';
+ }
if (c->resp_too_large)
{
(axis-axis2-c-core) 05/06: Bound a whole h2 client post with request_timeout_ms
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit ca03d215d96ab94f73bf2d6f1d0a27ad6040d339
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Thu Sep 24 15:34:21 2026 -1000
Bound a whole h2 client post with request_timeout_ms
io_timeout_ms limits each wait, so a server trickling bytes could hold a
post indefinitely. The new option (default 120 s) caps every wait at the
post's overall deadline. Connecting stays under connect_timeout_ms.
Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
docs/HTTP2_JSON_CLIENT.md | 2 +-
include/axis2_h2_json_client.h | 7 +++
.../transport/h2/sender/axis2_h2_json_client.c | 65 ++++++++++++++++++----
3 files changed, 63 insertions(+), 11 deletions(-)
diff --git a/docs/HTTP2_JSON_CLIENT.md b/docs/HTTP2_JSON_CLIENT.md
index da8fb423c..3403e61f8 100644
--- a/docs/HTTP2_JSON_CLIENT.md
+++ b/docs/HTTP2_JSON_CLIENT.md
@@ -57,7 +57,7 @@ next. A client is not thread-safe; use one per thread.
| TLS 1.2 or later, no compression, no renegotiation | RFC 9113 §9.2. |
| The server certificate is always verified, including its name | There is no option to skip it. |
| `verify_name` is separate from `host` | Connecting by IP to a server whose certificate names it by DNS name — the equivalent of `curl --resolve`. An IP-literal `verify_name` is matched against IP SANs and never sent as SNI. |
-| Every wait has a deadline | Connect + handshake share `connect_timeout_ms` (default 5 s); each wait for the server is bounded by `io_timeout_ms` (default 30 s). |
+| Every wait has a deadline | Connect + handshake share `connect_timeout_ms` (default 5 s); each wait for the server is bounded by `io_timeout_ms` (default 30 s), and the whole request and response by `request_timeout_ms` (default 120 s), so a server trickling bytes cannot hold a post. |
| A response over `max_response_bytes` is abandoned, not truncated | Default 16 MB, the server's JSON transport buffer. The stream is reset. |
| Nothing is retried | A POST may not be safe to repeat. After a failure the connection is dropped and the next post opens a new one. |
| A connection the server closed while idle is replaced before the next request | Servers close idle HTTP/2 connections (httpd's keep-alive timeout is 5 s by default). The client checks, without waiting, for a GOAWAY or FIN before it sends; nothing has been sent, so a new connection is not a retry. |
diff --git a/include/axis2_h2_json_client.h b/include/axis2_h2_json_client.h
index 9dd6b9b0f..4d9e7ea80 100644
--- a/include/axis2_h2_json_client.h
+++ b/include/axis2_h2_json_client.h
@@ -84,6 +84,13 @@ extern "C"
/** Longest wait for the server to make progress. 0 means 30000 ms. */
int io_timeout_ms;
+ /**
+ * Longest a post may take from sending the request to receiving the
+ * whole response, however steadily the server makes progress.
+ * Connecting is bounded separately. 0 means 120000 ms.
+ */
+ int request_timeout_ms;
+
/**
* Largest response body accepted. 0 means 16 MB, the size of the
* server's JSON transport buffer; a larger response is abandoned.
diff --git a/src/core/transport/h2/sender/axis2_h2_json_client.c b/src/core/transport/h2/sender/axis2_h2_json_client.c
index c330a560d..1ae794a16 100644
--- a/src/core/transport/h2/sender/axis2_h2_json_client.c
+++ b/src/core/transport/h2/sender/axis2_h2_json_client.c
@@ -78,10 +78,12 @@
#define H2C_DEFAULT_CONNECT_MS 5000
#define H2C_DEFAULT_IO_MS 30000
+#define H2C_DEFAULT_REQUEST_MS 120000
#define H2C_DEFAULT_MAX_RESPONSE ((size_t)16 * 1024 * 1024)
#define H2C_IO_CHUNK 16384
#define H2C_STREAM_WINDOW (1 << 20) /* per-stream receive window */
#define H2C_CONNECTION_WINDOW (1 << 24) /* connection receive window */
+#define H2C_NO_DEADLINE ((int64_t)-1)
struct axis2_h2_json_client
{
@@ -93,6 +95,7 @@ struct axis2_h2_json_client
axis2_char_t *authority;
int connect_timeout_ms;
int io_timeout_ms;
+ int request_timeout_ms;
size_t max_response;
SSL_CTX *ctx;
@@ -106,6 +109,8 @@ struct axis2_h2_json_client
/* the request in flight -- written by the nghttp2 callbacks, read by
* axis2_h2_json_client_post once the stream closes */
const axutil_env_t *env;
+ int64_t request_deadline; /* end of request_timeout_ms; H2C_NO_DEADLINE
+ * outside a post */
int32_t stream_id;
int stream_closed; /* set by h2c_on_stream_close */
uint32_t stream_error; /* RST_STREAM code; NGHTTP2_NO_ERROR if clean */
@@ -185,16 +190,28 @@ h2c_wait(int fd, short events, int timeout_ms)
}
}
+/* How long the next wait for the server may last. io_timeout_ms alone
+ * bounds each wait, not the post: a server trickling a byte per wait could
+ * hold a post for io_timeout_ms times the response size. So within a post
+ * every wait is also cut short at request_deadline. <= 0: it has passed. */
+static int
+h2c_io_wait_ms(const axis2_h2_json_client_t *c)
+{
+ int64_t left;
+ if (c->request_deadline == H2C_NO_DEADLINE)
+ return c->io_timeout_ms;
+ left = c->request_deadline - h2c_now_ms();
+ return left < c->io_timeout_ms ? (int)left : c->io_timeout_ms;
+}
+
/* ------------------------------------------------------------------------ */
/* socket I/O under the memory BIOs */
/* ------------------------------------------------------------------------ */
-/* deadline for h2c_send_all / h2c_flush: none, so each wait for the socket
- * to drain gets io_timeout_ms. The handshake passes its connect deadline
- * instead, so a server that stalls its receive window cannot stretch
- * connect_timeout_ms to io_timeout_ms. */
-#define H2C_NO_DEADLINE ((int64_t)-1)
-
+/* deadline for h2c_send_all / h2c_flush: H2C_NO_DEADLINE means each wait
+ * for the socket to drain gets h2c_io_wait_ms. The handshake passes its
+ * connect deadline instead, so a server that stalls its receive window
+ * cannot stretch connect_timeout_ms to io_timeout_ms. */
static axis2_status_t
h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
const char *buf, size_t len, int64_t deadline)
@@ -212,7 +229,7 @@ h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
{
- int64_t left = deadline == H2C_NO_DEADLINE ? c->io_timeout_ms
+ int64_t left = deadline == H2C_NO_DEADLINE ? h2c_io_wait_ms(c)
: deadline - h2c_now_ms();
int w = left > 0 ? h2c_wait(c->fd, POLLOUT, (int)left) : 0;
if (w > 0)
@@ -464,11 +481,22 @@ h2c_pump_recv(axis2_h2_json_client_t *c, const axutil_env_t *env)
case SSL_ERROR_WANT_READ:
if (got_any)
return AXIS2_SUCCESS;
+ {
+ int wait_ms;
/* TLS may owe the server something first (a key update). */
- if (h2c_flush(c, env, H2C_NO_DEADLINE) != AXIS2_SUCCESS ||
- h2c_fill(c, env, c->io_timeout_ms) != AXIS2_SUCCESS)
+ if (h2c_flush(c, env, H2C_NO_DEADLINE) != AXIS2_SUCCESS)
+ return AXIS2_FAILURE;
+ wait_ms = h2c_io_wait_ms(c);
+ if (wait_ms <= 0)
+ {
+ h2c_fail(c, env, "no complete response within %d ms",
+ c->request_timeout_ms);
+ return AXIS2_FAILURE;
+ }
+ if (h2c_fill(c, env, wait_ms) != AXIS2_SUCCESS)
return AXIS2_FAILURE;
break;
+ }
case SSL_ERROR_ZERO_RETURN:
/* close_notify can arrive in the same read as the end of the
* response. Let the caller see what was decoded first; if
@@ -827,6 +855,9 @@ axis2_h2_json_client_create(
c->port = o->port;
c->connect_timeout_ms = o->connect_timeout_ms > 0 ? o->connect_timeout_ms : H2C_DEFAULT_CONNECT_MS;
c->io_timeout_ms = o->io_timeout_ms > 0 ? o->io_timeout_ms : H2C_DEFAULT_IO_MS;
+ c->request_timeout_ms = o->request_timeout_ms > 0 ? o->request_timeout_ms
+ : H2C_DEFAULT_REQUEST_MS;
+ c->request_deadline = H2C_NO_DEADLINE; /* 0 would read as expired */
c->max_response = o->max_response_bytes > 0 ? o->max_response_bytes : H2C_DEFAULT_MAX_RESPONSE;
if (c->max_response == SIZE_MAX)
c->max_response = SIZE_MAX - 1; /* room for the terminating NUL */
@@ -942,7 +973,10 @@ axis2_h2_json_client_post(
return AXIS2_FAILURE;
}
+ /* Connecting is bounded by connect_timeout_ms above; the clock for the
+ * request itself starts here. */
c->env = env;
+ c->request_deadline = h2c_now_ms() + c->request_timeout_ms;
c->stream_closed = 0;
c->stream_error = 0;
c->http_status = 0;
@@ -991,7 +1025,16 @@ axis2_h2_json_client_post(
/* Alternate: send what nghttp2 has queued (headers, body as the window
* opens, WINDOW_UPDATEs, SETTINGS ACKs), then wait for the server,
* until the callbacks mark the stream closed. If the session wants
- * neither to read nor write, the server has GOAWAYed us mid-request. */
+ * neither to read nor write, the server has GOAWAYed us mid-request.
+ *
+ * Sending before receiving cannot deadlock against a conforming server.
+ * nghttp2 releases DATA only within the flow-control window the server
+ * granted, and the server keeps reading the connection to process
+ * frames. A server that answers early (a 413 before the body is all
+ * sent) sends its response and RST_STREAM(NO_ERROR) (RFC 9113 8.1);
+ * the next h2c_pump_recv takes both, and nghttp2 sends no more body. A
+ * server that stops reading altogether blocks a send only until
+ * request_deadline. */
while (!c->stream_closed)
{
if (h2c_pump_send(c, env) != AXIS2_SUCCESS)
@@ -1053,6 +1096,7 @@ axis2_h2_json_client_post(
*http_status_out = c->http_status;
c->resp = NULL;
c->req_body = NULL;
+ c->request_deadline = H2C_NO_DEADLINE;
return AXIS2_SUCCESS;
fail:
@@ -1060,6 +1104,7 @@ fail:
AXIS2_FREE(env->allocator, c->resp);
c->resp = NULL;
c->req_body = NULL;
+ c->request_deadline = H2C_NO_DEADLINE;
h2c_disconnect(c, env, 0);
return AXIS2_FAILURE;
}
(axis-axis2-c-core) 02/06: Reopen an HTTP/2 connection the server closed while it was idle
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit 95fff174abb608ad0d0198c1affedd7106c4a4ef
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Sep 22 16:30:40 2026 -1000
Reopen an HTTP/2 connection the server closed while it was idle
httpd closes idle HTTP/2 connections after its keep-alive timeout, and the
client found out only by failing the next post. Before sending on a reused
connection it now checks, without waiting, for a GOAWAY or FIN and opens
a new connection; nothing has been sent, so this is not a retry.
Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
docs/HTTP2_JSON_CLIENT.md | 5 +-
.../transport/h2/sender/axis2_h2_json_client.c | 66 ++++++++++++++++++++--
src/core/transport/h2/test/h2_json_client_test.c | 22 ++++++++
3 files changed, 88 insertions(+), 5 deletions(-)
diff --git a/docs/HTTP2_JSON_CLIENT.md b/docs/HTTP2_JSON_CLIENT.md
index af24698fd..da8fb423c 100644
--- a/docs/HTTP2_JSON_CLIENT.md
+++ b/docs/HTTP2_JSON_CLIENT.md
@@ -60,6 +60,7 @@ next. A client is not thread-safe; use one per thread.
| Every wait has a deadline | Connect + handshake share `connect_timeout_ms` (default 5 s); each wait for the server is bounded by `io_timeout_ms` (default 30 s). |
| A response over `max_response_bytes` is abandoned, not truncated | Default 16 MB, the server's JSON transport buffer. The stream is reset. |
| Nothing is retried | A POST may not be safe to repeat. After a failure the connection is dropped and the next post opens a new one. |
+| A connection the server closed while idle is replaced before the next request | Servers close idle HTTP/2 connections (httpd's keep-alive timeout is 5 s by default). The client checks, without waiting, for a GOAWAY or FIN before it sends; nothing has been sent, so a new connection is not a retry. |
| Any HTTP status is a success | A 500 from the service is still an answer; FAILURE means no complete response arrived. |
Socket I/O runs under OpenSSL memory BIOs, so a write to a closed peer cannot
@@ -81,7 +82,9 @@ AXIS2_H2_TEST_NAME=service.local \
The default path is the financial benchmark sample's `portfolioVariance`;
`AXIS2_H2_TEST_PATH` names another. The live checks cover three posts on one
connection, a response over the size limit, and a certificate that does not
-carry `verify_name`.
+carry `verify_name`. With `AXIS2_H2_TEST_IDLE_SECS` set longer than the
+server's keep-alive timeout they also post after the server has closed the idle
+connection.
## Not yet
diff --git a/src/core/transport/h2/sender/axis2_h2_json_client.c b/src/core/transport/h2/sender/axis2_h2_json_client.c
index d81de23ef..2a5734347 100644
--- a/src/core/transport/h2/sender/axis2_h2_json_client.c
+++ b/src/core/transport/h2/sender/axis2_h2_json_client.c
@@ -655,6 +655,58 @@ h2c_connect(axis2_h2_json_client_t *c, const axutil_env_t *env)
return AXIS2_SUCCESS;
}
+/* Before a new request on a reused connection: did the server end it while
+ * we were idle? Servers routinely close idle HTTP/2 connections (httpd's
+ * keep-alive timeout is 5 s by default) with GOAWAY and a FIN; finding that
+ * out by failing the next request would fail every first call after a
+ * pause. Nothing of the new request has been sent yet, so opening a fresh
+ * connection here is not a retry. Returns 1 when the connection is unusable. */
+static int
+h2c_peer_gone(axis2_h2_json_client_t *c, const axutil_env_t *env)
+{
+ struct pollfd p;
+ char buf[H2C_IO_CHUNK];
+ ssize_t n;
+
+ p.fd = c->fd;
+ p.events = POLLIN;
+ p.revents = 0;
+ if (poll(&p, 1, 0) <= 0)
+ return 0; /* nothing waiting: still open */
+ do
+ n = recv(c->fd, buf, 1, MSG_PEEK);
+ while (n < 0 && errno == EINTR);
+ if (n == 0)
+ return 1; /* FIN */
+ if (n < 0)
+ return !(errno == EAGAIN || errno == EWOULDBLOCK);
+
+ /* Bytes are waiting -- GOAWAY, PING, a TLS close_notify. Take them in
+ * and let nghttp2 decide. */
+ if (h2c_fill(c, env, 0) != AXIS2_SUCCESS)
+ return 1;
+ for (;;)
+ {
+ int r = SSL_read(c->ssl, buf, (int)sizeof(buf));
+ if (r > 0)
+ {
+#ifdef H2C_NGHTTP2_V2
+ if (nghttp2_session_mem_recv2(c->session, (const uint8_t *)buf, (size_t)r) < 0)
+#else
+ if (nghttp2_session_mem_recv(c->session, (const uint8_t *)buf, (size_t)r) < 0)
+#endif
+ return 1;
+ continue;
+ }
+ if (SSL_get_error(c->ssl, r) != SSL_ERROR_WANT_READ)
+ return 1; /* close_notify or a TLS error */
+ break;
+ }
+ if (h2c_pump_send(c, env) != AXIS2_SUCCESS)
+ return 1;
+ return !nghttp2_session_want_read(c->session) && !nghttp2_session_want_write(c->session);
+}
+
/* ------------------------------------------------------------------------ */
/* public API */
/* ------------------------------------------------------------------------ */
@@ -797,11 +849,17 @@ axis2_h2_json_client_post(
return AXIS2_FAILURE;
}
- /* A session the server has finished with (GOAWAY) cannot carry a new
- * stream; open a fresh one. */
- if (c->session && !nghttp2_session_want_read(c->session) &&
- !nghttp2_session_want_write(c->session))
+ /* A session the server has finished with cannot carry a new stream;
+ * open a fresh one. The error text of a closed idle connection is not
+ * this call's failure, so it is cleared. */
+ if (c->session && h2c_peer_gone(c, env))
+ {
+ AXIS2_LOG_DEBUG(env->log, AXIS2_LOG_SI,
+ "[h2_json_client] %s:%d closed the idle connection; reconnecting",
+ c->host, c->port);
h2c_disconnect(c, env, 0);
+ c->error[0] = '\0';
+ }
if (!c->session && h2c_connect(c, env) != AXIS2_SUCCESS)
{
h2c_disconnect(c, env, 0);
diff --git a/src/core/transport/h2/test/h2_json_client_test.c b/src/core/transport/h2/test/h2_json_client_test.c
index 03eedd3bf..d88cfe3fe 100644
--- a/src/core/transport/h2/test/h2_json_client_test.c
+++ b/src/core/transport/h2/test/h2_json_client_test.c
@@ -28,6 +28,8 @@
* AXIS2_H2_TEST_NAME name the server certificate carries, if not HOST
* AXIS2_H2_TEST_PATH operation path (default: the financial benchmark
* sample's portfolioVariance)
+ * AXIS2_H2_TEST_IDLE_SECS also check a post after this long idle, longer
+ * than the server's keep-alive timeout (httpd: 5 s)
*/
#include <axis2_h2_json_client.h>
@@ -38,6 +40,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <unistd.h>
static int failures = 0;
@@ -170,6 +173,25 @@ live_checks(const axutil_env_t *env)
}
}
+ /* A server that closed the connection while we were idle is noticed
+ * before the next request is sent, and a new connection is opened. */
+ if (getenv("AXIS2_H2_TEST_IDLE_SECS"))
+ {
+ int secs = atoi(getenv("AXIS2_H2_TEST_IDLE_SECS"));
+ axis2_status_t s;
+ printf(" idle %d s...\n", secs);
+ sleep((unsigned)secs);
+ s = axis2_h2_json_client_post(c, env, path, PV_BODY, strlen(PV_BODY), &resp, &len, &status);
+ CHECK(s == AXIS2_SUCCESS && status == 200, "a post after the server closed an idle connection");
+ if (s != AXIS2_SUCCESS)
+ printf(" error: %s\n", axis2_h2_json_client_get_error(c));
+ if (resp)
+ {
+ AXIS2_FREE(env->allocator, resp);
+ resp = NULL;
+ }
+ }
+
/* A response larger than the cap is abandoned, not truncated. */
axis2_h2_json_client_free(c, env);
fill_live_options(&o);
(axis-axis2-c-core) 01/06: Add an HTTP/2 JSON client, libaxis2_h2_client
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit b98ad331618a24159547b1a5a9265f1f3c3bb493
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Sep 22 15:54:07 2026 -1000
Add an HTTP/2 JSON client, libaxis2_h2_client
Axis2/C could serve JSON over HTTP/2 but not call it. The new client POSTs
JSON to a service operation over nghttp2 and OpenSSL, requires ALPN h2,
always verifies the server name, and supports mTLS. It needs only axutil,
nghttp2 and OpenSSL, so embedded and Android callers link just that.
Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
docs/HTTP2_JSON_CLIENT.md | 91 ++
include/axis2_h2_json_client.h | 152 ++++
src/core/transport/h2/sender/Makefile.am | 17 +-
.../transport/h2/sender/axis2_h2_json_client.c | 942 +++++++++++++++++++++
src/core/transport/h2/test/Makefile.am | 16 +-
src/core/transport/h2/test/h2_json_client_test.c | 217 +++++
6 files changed, 1431 insertions(+), 4 deletions(-)
diff --git a/docs/HTTP2_JSON_CLIENT.md b/docs/HTTP2_JSON_CLIENT.md
new file mode 100644
index 000000000..af24698fd
--- /dev/null
+++ b/docs/HTTP2_JSON_CLIENT.md
@@ -0,0 +1,91 @@
+# HTTP/2 JSON Client
+
+`axis2_h2_json_client.h` is the client half of the HTTP/2 JSON transport: it
+POSTs a JSON body to one operation of an Axis2/C service and returns the JSON
+response. Until it existed, Axis2/C could serve JSON over HTTP/2 (mod_axis2
+under mod_http2) but could not call such a service; the HTTP/2 transport
+sender in `src/core/transport/h2/sender/axis2_h2_transport_sender.c` is still
+a placeholder.
+
+The library is `libaxis2_h2_client`, built with `--enable-http2 --enable-json`.
+It depends on **axutil, nghttp2 and OpenSSL only** — no engine, no axiom, no
+`axis2.xml` — so an embedded or mobile caller links what it uses. On Android it
+cross-compiles with the NDK against the same nghttp2 and OpenSSL the server
+build uses.
+
+## Use
+
+```c
+#include <axis2_h2_json_client.h>
+
+axis2_h2_json_client_options_t o = {0};
+o.host = "192.168.1.20"; /* connect address */
+o.port = 8443;
+o.verify_name = "service.local"; /* name the server cert carries */
+o.ca_file = "ca.crt";
+o.cert_file = "client.crt"; /* mTLS; omit for none */
+o.key_file = "client.key";
+
+axis2_h2_json_client_t *c = axis2_h2_json_client_create(env, &o);
+
+const char *body = "{\"n_assets\":2,\"weights\":[0.5,0.5],"
+ "\"covariance_matrix\":[[0.04,0.01],[0.01,0.09]]}";
+axis2_char_t *resp = NULL;
+size_t len = 0;
+int status = 0;
+
+if (axis2_h2_json_client_post(c, env,
+ "/services/FinancialBenchmarkService/portfolioVariance",
+ body, strlen(body), &resp, &len, &status) == AXIS2_SUCCESS) {
+ /* status is the HTTP status; resp is the JSON body, NUL-terminated */
+ AXIS2_FREE(env->allocator, resp);
+} else {
+ printf("%s\n", axis2_h2_json_client_get_error(c));
+}
+
+axis2_h2_json_client_free(c, env);
+```
+
+One client holds one connection, opened on the first post and reused by the
+next. A client is not thread-safe; use one per thread.
+
+## Rules it enforces
+
+| Rule | Why |
+|---|---|
+| ALPN must select `h2`; an HTTP/1.1-only server is refused | Axis2/C serves JSON over HTTP/2 only. A silent downgrade would fail later and somewhere less obvious. |
+| TLS 1.2 or later, no compression, no renegotiation | RFC 9113 §9.2. |
+| The server certificate is always verified, including its name | There is no option to skip it. |
+| `verify_name` is separate from `host` | Connecting by IP to a server whose certificate names it by DNS name — the equivalent of `curl --resolve`. An IP-literal `verify_name` is matched against IP SANs and never sent as SNI. |
+| Every wait has a deadline | Connect + handshake share `connect_timeout_ms` (default 5 s); each wait for the server is bounded by `io_timeout_ms` (default 30 s). |
+| A response over `max_response_bytes` is abandoned, not truncated | Default 16 MB, the server's JSON transport buffer. The stream is reset. |
+| Nothing is retried | A POST may not be safe to repeat. After a failure the connection is dropped and the next post opens a new one. |
+| Any HTTP status is a success | A 500 from the service is still an answer; FAILURE means no complete response arrived. |
+
+Socket I/O runs under OpenSSL memory BIOs, so a write to a closed peer cannot
+raise `SIGPIPE` and the library never changes the process's signal handling.
+
+## Test
+
+`make check` in `src/core/transport/h2/test` runs `h2_json_client_test`.
+Its offline checks always run. The live checks need a real server and run
+only when one is named:
+
+```sh
+AXIS2_H2_TEST_HOST=192.168.1.20 AXIS2_H2_TEST_PORT=8443 \
+AXIS2_H2_TEST_CA=ca.crt AXIS2_H2_TEST_CERT=client.crt AXIS2_H2_TEST_KEY=client.key \
+AXIS2_H2_TEST_NAME=service.local \
+ ./h2_json_client_test
+```
+
+The default path is the financial benchmark sample's `portfolioVariance`;
+`AXIS2_H2_TEST_PATH` names another. The live checks cover three posts on one
+connection, a response over the size limit, and a certificate that does not
+carry `verify_name`.
+
+## Not yet
+
+- A transport sender behind `axis2_svc_client` built on this client, so
+ existing client code can reach HTTP/2 JSON services unchanged.
+- Concurrent streams on one connection. The client sends one request at a
+ time; HTTP/2 multiplexing is available underneath if a caller needs it.
diff --git a/include/axis2_h2_json_client.h b/include/axis2_h2_json_client.h
new file mode 100644
index 000000000..9dd6b9b0f
--- /dev/null
+++ b/include/axis2_h2_json_client.h
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+#ifndef AXIS2_H2_JSON_CLIENT_H
+#define AXIS2_H2_JSON_CLIENT_H
+
+/**
+ * @file axis2_h2_json_client.h
+ * @brief JSON over HTTP/2, the client half of the HTTP/2 JSON transport.
+ *
+ * Posts a JSON body to one operation of an Axis2/C service (for example
+ * /services/FinancialBenchmarkService/portfolioVariance) and returns the JSON
+ * response. It is the counterpart of mod_axis2 serving JSON over mod_http2,
+ * and depends only on axutil, nghttp2 and OpenSSL -- no engine, no axiom --
+ * so an embedded or mobile caller links what it uses.
+ *
+ * HTTP/2 only. ALPN must select "h2"; a server that offers only HTTP/1.1 is
+ * refused rather than spoken to, because Axis2/C serves JSON over HTTP/2 and
+ * a silent downgrade would fail later and somewhere less obvious.
+ *
+ * TLS is mandatory and the server certificate is always verified, including
+ * its name. A client certificate is presented when one is configured (mTLS).
+ *
+ * One client holds one connection, opened on the first post and reused by
+ * later ones. A client is not thread-safe: use one per thread.
+ */
+
+#include <axutil_utils.h>
+#include <axutil_env.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ /** Opaque client. */
+ typedef struct axis2_h2_json_client axis2_h2_json_client_t;
+
+ /**
+ * Connection and TLS settings. Strings are copied at create time.
+ */
+ typedef struct axis2_h2_json_client_options
+ {
+ /** Address to connect to: an IP literal or a DNS name. Required. */
+ const axis2_char_t *host;
+
+ /** TCP port. Required. */
+ int port;
+
+ /**
+ * Name the server certificate must carry, and the TLS SNI and
+ * :authority sent. NULL means host. Set it when connecting by IP to a
+ * server whose certificate names it by DNS name -- the equivalent of
+ * curl --resolve. Verification is never skipped.
+ */
+ const axis2_char_t *verify_name;
+
+ /** PEM file of the CA(s) the server certificate must chain to. Required. */
+ const axis2_char_t *ca_file;
+
+ /** PEM client certificate (chain) for mTLS, or NULL for none. */
+ const axis2_char_t *cert_file;
+
+ /** PEM private key for cert_file. Required when cert_file is set. */
+ const axis2_char_t *key_file;
+
+ /** TCP connect and TLS handshake limit. 0 means 5000 ms. */
+ int connect_timeout_ms;
+
+ /** Longest wait for the server to make progress. 0 means 30000 ms. */
+ int io_timeout_ms;
+
+ /**
+ * Largest response body accepted. 0 means 16 MB, the size of the
+ * server's JSON transport buffer; a larger response is abandoned.
+ */
+ size_t max_response_bytes;
+ } axis2_h2_json_client_options_t;
+
+ /**
+ * Create a client. No connection is made until the first post.
+ * @return the client, or NULL if a required option is missing or invalid
+ * (the reason is logged).
+ */
+ AXIS2_EXTERN axis2_h2_json_client_t *AXIS2_CALL
+ axis2_h2_json_client_create(
+ const axutil_env_t *env,
+ const axis2_h2_json_client_options_t *options);
+
+ /**
+ * POST a JSON body and wait for the whole response.
+ *
+ * @param path request path, starting with '/'
+ * @param json request body; need not be NUL-terminated
+ * @param json_len its length in bytes
+ * @param response_out set to the response body, NUL-terminated, allocated
+ * with env->allocator; the caller frees it. Set to
+ * NULL on failure.
+ * @param response_len_out set to the body length (may be NULL)
+ * @param http_status_out set to the HTTP status (may be NULL)
+ * @return AXIS2_SUCCESS when a complete response arrived, whatever its
+ * status -- a 500 from the service is still an answer. FAILURE
+ * for anything that prevented one: connect, TLS, ALPN, timeout,
+ * reset stream, or a response over the size limit. The reason is
+ * in axis2_h2_json_client_get_error. The connection is dropped
+ * after a failure and reopened by the next post; nothing is
+ * retried automatically, since a POST may not be safe to repeat.
+ */
+ AXIS2_EXTERN axis2_status_t AXIS2_CALL
+ axis2_h2_json_client_post(
+ axis2_h2_json_client_t *client,
+ const axutil_env_t *env,
+ const axis2_char_t *path,
+ const axis2_char_t *json,
+ size_t json_len,
+ axis2_char_t **response_out,
+ size_t *response_len_out,
+ int *http_status_out);
+
+ /**
+ * Why the last call failed, or "" after a success. Owned by the client;
+ * valid until the next call.
+ */
+ AXIS2_EXTERN const axis2_char_t *AXIS2_CALL
+ axis2_h2_json_client_get_error(
+ const axis2_h2_json_client_t *client);
+
+ /** Close the connection and free the client. NULL is ignored. */
+ AXIS2_EXTERN void AXIS2_CALL
+ axis2_h2_json_client_free(
+ axis2_h2_json_client_t *client,
+ const axutil_env_t *env);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* AXIS2_H2_JSON_CLIENT_H */
diff --git a/src/core/transport/h2/sender/Makefile.am b/src/core/transport/h2/sender/Makefile.am
index 2ffce9f0f..9f05bf453 100644
--- a/src/core/transport/h2/sender/Makefile.am
+++ b/src/core/transport/h2/sender/Makefile.am
@@ -25,4 +25,19 @@ libaxis2_h2_sender_la_CPPFLAGS = -I$(top_srcdir)/include \
-I$(top_srcdir)/src/core/context \
-I$(top_srcdir)/src/core/transport/http/util \
$(NGHTTP2_CFLAGS) \
- $(JSON_CFLAGS)
\ No newline at end of file
+ $(JSON_CFLAGS)
+
+# JSON over HTTP/2 client (axis2_h2_json_client.h). Installed on its own and
+# kept free of the engine and axiom: it needs only axutil, nghttp2 and OpenSSL,
+# so an embedded caller links exactly that.
+lib_LTLIBRARIES = libaxis2_h2_client.la
+libaxis2_h2_client_la_SOURCES = axis2_h2_json_client.c
+libaxis2_h2_client_la_CPPFLAGS = -I$(top_srcdir)/include \
+ -I$(top_srcdir)/util/include \
+ $(NGHTTP2_CFLAGS) \
+ $(OPENSSL_CFLAGS) \
+ @OPENSSLINC@
+libaxis2_h2_client_la_LIBADD = $(top_builddir)/util/src/libaxutil.la \
+ $(NGHTTP2_LIBS) \
+ $(OPENSSL_LIBS)
+libaxis2_h2_client_la_LDFLAGS = -version-info $(VERSION_NO)
diff --git a/src/core/transport/h2/sender/axis2_h2_json_client.c b/src/core/transport/h2/sender/axis2_h2_json_client.c
new file mode 100644
index 000000000..d81de23ef
--- /dev/null
+++ b/src/core/transport/h2/sender/axis2_h2_json_client.c
@@ -0,0 +1,942 @@
+/*
+ * 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.
+ */
+
+/*
+ * JSON over HTTP/2 client. See axis2_h2_json_client.h for the contract.
+ *
+ * Structure: a non-blocking TCP socket, OpenSSL driven through a pair of
+ * memory BIOs, and an nghttp2 client session fed with mem_send / mem_recv.
+ * The memory BIOs keep every byte of socket I/O in this file, so each wait is
+ * a poll() with a deadline, a write to a closed peer cannot raise SIGPIPE
+ * (send() with MSG_NOSIGNAL), and nothing ever blocks without a timeout.
+ */
+
+#include <axis2_h2_json_client.h>
+#include <axutil_string.h>
+#include <axutil_log.h>
+
+#include <nghttp2/nghttp2.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/x509v3.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <arpa/inet.h>
+#include <poll.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#error "The HTTP/2 JSON client requires OpenSSL 1.1.0 or later"
+#endif
+
+/* nghttp2 1.60 replaced mem_send, mem_recv, submit_request and the data
+ * provider with ssize-returning "2" variants and deprecated the originals. */
+#if NGHTTP2_VERSION_NUM >= 0x013c00
+#define H2C_NGHTTP2_V2 1
+#endif
+
+#ifndef MSG_NOSIGNAL
+#define MSG_NOSIGNAL 0 /* no such flag: SO_NOSIGPIPE is set instead */
+#endif
+
+#define H2C_DEFAULT_CONNECT_MS 5000
+#define H2C_DEFAULT_IO_MS 30000
+#define H2C_DEFAULT_MAX_RESPONSE ((size_t)16 * 1024 * 1024)
+#define H2C_IO_CHUNK 16384
+#define H2C_STREAM_WINDOW (1 << 20) /* per-stream receive window */
+#define H2C_CONNECTION_WINDOW (1 << 24) /* connection receive window */
+
+struct axis2_h2_json_client
+{
+ /* configuration, copied at create */
+ axis2_char_t *host;
+ int port;
+ axis2_char_t *verify_name;
+ int verify_name_is_ip;
+ axis2_char_t *authority;
+ int connect_timeout_ms;
+ int io_timeout_ms;
+ size_t max_response;
+ SSL_CTX *ctx;
+
+ /* connection */
+ int fd;
+ SSL *ssl;
+ BIO *rbio; /* network -> TLS; owned by ssl */
+ BIO *wbio; /* TLS -> network; owned by ssl */
+ nghttp2_session *session;
+
+ /* the request in flight */
+ const axutil_env_t *env;
+ int32_t stream_id;
+ int stream_closed;
+ uint32_t stream_error;
+ int http_status;
+ const axis2_char_t *req_body;
+ size_t req_len;
+ size_t req_off;
+ axis2_char_t *resp;
+ size_t resp_len;
+ size_t resp_cap;
+ int resp_too_large;
+
+ axis2_char_t error[256];
+};
+
+/* ------------------------------------------------------------------------ */
+/* errors and time */
+/* ------------------------------------------------------------------------ */
+
+static void
+h2c_fail(axis2_h2_json_client_t *c, const axutil_env_t *env, const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(c->error, sizeof(c->error), fmt, ap);
+ va_end(ap);
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "[h2_json_client] %s:%d: %s",
+ c->host, c->port, c->error);
+}
+
+/* The first queued OpenSSL error as text, or "" -- and clear the queue so a
+ * stale entry cannot be reported against a later failure. */
+static const char *
+h2c_ssl_reason(char *buf, size_t len)
+{
+ unsigned long e = ERR_get_error();
+ buf[0] = '\0';
+ if (e)
+ ERR_error_string_n(e, buf, len);
+ ERR_clear_error();
+ return buf;
+}
+
+static long
+h2c_now_ms(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+}
+
+/* poll() one descriptor, retrying on EINTR against the same deadline.
+ * 1 ready, 0 timed out, -1 error. */
+static int
+h2c_wait(int fd, short events, int timeout_ms)
+{
+ long deadline = h2c_now_ms() + timeout_ms;
+ for (;;)
+ {
+ struct pollfd p;
+ long left = deadline - h2c_now_ms();
+ int r;
+ if (left < 0)
+ left = 0;
+ p.fd = fd;
+ p.events = events;
+ p.revents = 0;
+ r = poll(&p, 1, (int)left);
+ if (r > 0)
+ return 1;
+ if (r == 0)
+ return 0;
+ if (errno != EINTR)
+ return -1;
+ }
+}
+
+/* ------------------------------------------------------------------------ */
+/* socket I/O under the memory BIOs */
+/* ------------------------------------------------------------------------ */
+
+static axis2_status_t
+h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
+ const char *buf, size_t len)
+{
+ while (len > 0)
+ {
+ ssize_t n = send(c->fd, buf, len, MSG_NOSIGNAL);
+ if (n > 0)
+ {
+ buf += n;
+ len -= (size_t)n;
+ continue;
+ }
+ if (n < 0 && errno == EINTR)
+ continue;
+ if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ {
+ int w = h2c_wait(c->fd, POLLOUT, c->io_timeout_ms);
+ if (w > 0)
+ continue;
+ h2c_fail(c, env, w == 0 ? "timed out sending to the server"
+ : "poll failed while sending: %s", strerror(errno));
+ return AXIS2_FAILURE;
+ }
+ h2c_fail(c, env, "send failed: %s", n < 0 ? strerror(errno) : "no progress");
+ return AXIS2_FAILURE;
+ }
+ return AXIS2_SUCCESS;
+}
+
+/* Send whatever TLS has produced. */
+static axis2_status_t
+h2c_flush(axis2_h2_json_client_t *c, const axutil_env_t *env)
+{
+ char buf[H2C_IO_CHUNK];
+ int n;
+ while ((n = BIO_read(c->wbio, buf, (int)sizeof(buf))) > 0)
+ {
+ if (h2c_send_all(c, env, buf, (size_t)n) != AXIS2_SUCCESS)
+ return AXIS2_FAILURE;
+ }
+ return AXIS2_SUCCESS;
+}
+
+/* Wait up to timeout_ms for bytes from the server and hand them to TLS. */
+static axis2_status_t
+h2c_fill(axis2_h2_json_client_t *c, const axutil_env_t *env, int timeout_ms)
+{
+ char buf[H2C_IO_CHUNK];
+ ssize_t n;
+ int w = h2c_wait(c->fd, POLLIN, timeout_ms);
+ if (w == 0)
+ {
+ h2c_fail(c, env, "timed out after %d ms waiting for the server", timeout_ms);
+ return AXIS2_FAILURE;
+ }
+ if (w < 0)
+ {
+ h2c_fail(c, env, "poll failed while receiving: %s", strerror(errno));
+ return AXIS2_FAILURE;
+ }
+ do
+ n = recv(c->fd, buf, sizeof(buf), 0);
+ while (n < 0 && errno == EINTR);
+ if (n == 0)
+ {
+ h2c_fail(c, env, "the server closed the connection");
+ return AXIS2_FAILURE;
+ }
+ if (n < 0)
+ {
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return AXIS2_SUCCESS; /* spurious wakeup; the caller retries */
+ h2c_fail(c, env, "recv failed: %s", strerror(errno));
+ return AXIS2_FAILURE;
+ }
+ if (BIO_write(c->rbio, buf, (int)n) != (int)n)
+ {
+ h2c_fail(c, env, "could not buffer %d bytes for TLS", (int)n);
+ return AXIS2_FAILURE;
+ }
+ return AXIS2_SUCCESS;
+}
+
+/* ------------------------------------------------------------------------ */
+/* nghttp2 callbacks */
+/* ------------------------------------------------------------------------ */
+
+static int
+h2c_on_header(nghttp2_session *session, const nghttp2_frame *frame,
+ const uint8_t *name, size_t namelen,
+ const uint8_t *value, size_t valuelen,
+ uint8_t flags, void *user_data)
+{
+ axis2_h2_json_client_t *c = (axis2_h2_json_client_t *)user_data;
+ (void)session;
+ (void)flags;
+ if (frame->hd.type == NGHTTP2_HEADERS && frame->hd.stream_id == c->stream_id &&
+ namelen == 7 && memcmp(name, ":status", 7) == 0 && valuelen == 3)
+ {
+ c->http_status = (value[0] - '0') * 100 + (value[1] - '0') * 10 + (value[2] - '0');
+ }
+ return 0;
+}
+
+static int
+h2c_on_data(nghttp2_session *session, uint8_t flags, int32_t stream_id,
+ const uint8_t *data, size_t len, void *user_data)
+{
+ axis2_h2_json_client_t *c = (axis2_h2_json_client_t *)user_data;
+ (void)flags;
+ if (stream_id != c->stream_id || c->resp_too_large)
+ return 0;
+ if (len > c->max_response - c->resp_len)
+ {
+ /* Stop the server rather than keep buffering; the post reports it. */
+ c->resp_too_large = 1;
+ nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_CANCEL);
+ return 0;
+ }
+ if (c->resp_len + len + 1 > c->resp_cap)
+ {
+ size_t cap = c->resp_cap ? c->resp_cap : 4096;
+ axis2_char_t *grown;
+ while (cap < c->resp_len + len + 1)
+ cap *= 2;
+ grown = AXIS2_REALLOC(c->env->allocator, c->resp, cap);
+ if (!grown)
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
+ c->resp = grown;
+ c->resp_cap = cap;
+ }
+ memcpy(c->resp + c->resp_len, data, len);
+ c->resp_len += len;
+ return 0;
+}
+
+static int
+h2c_on_stream_close(nghttp2_session *session, int32_t stream_id,
+ uint32_t error_code, void *user_data)
+{
+ axis2_h2_json_client_t *c = (axis2_h2_json_client_t *)user_data;
+ (void)session;
+ if (stream_id == c->stream_id)
+ {
+ c->stream_closed = 1;
+ c->stream_error = error_code;
+ }
+ return 0;
+}
+
+#ifdef H2C_NGHTTP2_V2
+static nghttp2_ssize
+h2c_read_body(nghttp2_session *session, int32_t stream_id, uint8_t *buf,
+ size_t length, uint32_t *data_flags, nghttp2_data_source *source,
+ void *user_data)
+#else
+static ssize_t
+h2c_read_body(nghttp2_session *session, int32_t stream_id, uint8_t *buf,
+ size_t length, uint32_t *data_flags, nghttp2_data_source *source,
+ void *user_data)
+#endif
+{
+ axis2_h2_json_client_t *c = (axis2_h2_json_client_t *)user_data;
+ size_t left = c->req_len - c->req_off;
+ size_t n = left < length ? left : length;
+ (void)session;
+ (void)stream_id;
+ (void)source;
+ if (n > 0)
+ memcpy(buf, c->req_body + c->req_off, n);
+ c->req_off += n;
+ if (c->req_off == c->req_len)
+ *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+ return (int)n;
+}
+
+/* ------------------------------------------------------------------------ */
+/* session pump */
+/* ------------------------------------------------------------------------ */
+
+/* Move everything nghttp2 wants to send through TLS onto the socket. */
+static axis2_status_t
+h2c_pump_send(axis2_h2_json_client_t *c, const axutil_env_t *env)
+{
+ for (;;)
+ {
+ const uint8_t *data = NULL;
+#ifdef H2C_NGHTTP2_V2
+ nghttp2_ssize n = nghttp2_session_mem_send2(c->session, &data);
+#else
+ ssize_t n = nghttp2_session_mem_send(c->session, &data);
+#endif
+ if (n < 0)
+ {
+ h2c_fail(c, env, "HTTP/2 framing error: %s", nghttp2_strerror((int)n));
+ return AXIS2_FAILURE;
+ }
+ if (n == 0)
+ break;
+ if (SSL_write(c->ssl, data, (int)n) != (int)n)
+ {
+ char why[160];
+ h2c_fail(c, env, "TLS write failed: %s", h2c_ssl_reason(why, sizeof(why)));
+ return AXIS2_FAILURE;
+ }
+ }
+ return h2c_flush(c, env);
+}
+
+/* Read what the server has sent -- waiting up to io_timeout for the first
+ * byte -- and feed it to nghttp2. Returns once TLS has nothing more to give. */
+static axis2_status_t
+h2c_pump_recv(axis2_h2_json_client_t *c, const axutil_env_t *env)
+{
+ char buf[H2C_IO_CHUNK];
+ int got_any = 0;
+ for (;;)
+ {
+ int n = SSL_read(c->ssl, buf, (int)sizeof(buf));
+ if (n > 0)
+ {
+#ifdef H2C_NGHTTP2_V2
+ nghttp2_ssize rv = nghttp2_session_mem_recv2(c->session, (const uint8_t *)buf, (size_t)n);
+#else
+ ssize_t rv = nghttp2_session_mem_recv(c->session, (const uint8_t *)buf, (size_t)n);
+#endif
+ if (rv < 0)
+ {
+ h2c_fail(c, env, "HTTP/2 protocol error from the server: %s",
+ nghttp2_strerror((int)rv));
+ return AXIS2_FAILURE;
+ }
+ got_any = 1;
+ continue;
+ }
+ switch (SSL_get_error(c->ssl, n))
+ {
+ case SSL_ERROR_WANT_READ:
+ if (got_any)
+ return AXIS2_SUCCESS;
+ /* TLS may owe the server something first (a key update). */
+ if (h2c_flush(c, env) != AXIS2_SUCCESS ||
+ h2c_fill(c, env, c->io_timeout_ms) != AXIS2_SUCCESS)
+ return AXIS2_FAILURE;
+ break;
+ case SSL_ERROR_ZERO_RETURN:
+ h2c_fail(c, env, "the server closed the TLS session");
+ return AXIS2_FAILURE;
+ default:
+ {
+ char why[160];
+ h2c_fail(c, env, "TLS read failed: %s", h2c_ssl_reason(why, sizeof(why)));
+ return AXIS2_FAILURE;
+ }
+ }
+ }
+}
+
+/* ------------------------------------------------------------------------ */
+/* connect / disconnect */
+/* ------------------------------------------------------------------------ */
+
+static void
+h2c_disconnect(axis2_h2_json_client_t *c, const axutil_env_t *env, int graceful)
+{
+ if (graceful && c->session && c->ssl)
+ {
+ /* Best effort: GOAWAY and close_notify, without waiting for replies. */
+ axis2_char_t saved[sizeof(c->error)];
+ memcpy(saved, c->error, sizeof(saved));
+ nghttp2_session_terminate_session(c->session, NGHTTP2_NO_ERROR);
+ if (h2c_pump_send(c, env) == AXIS2_SUCCESS && SSL_shutdown(c->ssl) >= 0)
+ h2c_flush(c, env);
+ memcpy(c->error, saved, sizeof(saved));
+ ERR_clear_error();
+ }
+ if (c->session)
+ {
+ nghttp2_session_del(c->session);
+ c->session = NULL;
+ }
+ if (c->ssl)
+ {
+ SSL_free(c->ssl); /* frees both memory BIOs */
+ c->ssl = NULL;
+ c->rbio = NULL;
+ c->wbio = NULL;
+ }
+ if (c->fd >= 0)
+ {
+ close(c->fd);
+ c->fd = -1;
+ }
+}
+
+static axis2_status_t
+h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, long deadline)
+{
+ struct addrinfo hints, *res = NULL, *ai;
+ char port[16];
+ int rc, last_errno = 0;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ snprintf(port, sizeof(port), "%d", c->port);
+ rc = getaddrinfo(c->host, port, &hints, &res);
+ if (rc != 0)
+ {
+ h2c_fail(c, env, "cannot resolve %s: %s", c->host, gai_strerror(rc));
+ return AXIS2_FAILURE;
+ }
+
+ for (ai = res; ai; ai = ai->ai_next)
+ {
+ int fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ int one = 1, err = 0;
+ socklen_t errlen = sizeof(err);
+ long left;
+ if (fd < 0)
+ {
+ last_errno = errno;
+ continue;
+ }
+ fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
+#ifdef SO_NOSIGPIPE
+ setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
+#endif
+ if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0)
+ err = 0;
+ else if (errno != EINPROGRESS)
+ err = errno;
+ else
+ {
+ left = deadline - h2c_now_ms();
+ rc = h2c_wait(fd, POLLOUT, left > 0 ? (int)left : 0);
+ if (rc == 0)
+ err = ETIMEDOUT;
+ else if (rc < 0)
+ err = errno;
+ else if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) != 0)
+ err = errno;
+ }
+ if (err == 0)
+ {
+ setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+ c->fd = fd;
+ freeaddrinfo(res);
+ return AXIS2_SUCCESS;
+ }
+ last_errno = err;
+ close(fd);
+ }
+ freeaddrinfo(res);
+ h2c_fail(c, env, "cannot connect: %s", strerror(last_errno ? last_errno : ECONNREFUSED));
+ return AXIS2_FAILURE;
+}
+
+static axis2_status_t
+h2c_connect(axis2_h2_json_client_t *c, const axutil_env_t *env)
+{
+ long deadline = h2c_now_ms() + c->connect_timeout_ms;
+ const unsigned char *alpn = NULL;
+ unsigned int alpn_len = 0;
+ nghttp2_session_callbacks *cbs = NULL;
+ nghttp2_settings_entry settings[2];
+ char why[160];
+ int rv;
+
+ if (h2c_tcp_connect(c, env, deadline) != AXIS2_SUCCESS)
+ return AXIS2_FAILURE;
+
+ c->ssl = SSL_new(c->ctx);
+ c->rbio = BIO_new(BIO_s_mem());
+ c->wbio = BIO_new(BIO_s_mem());
+ if (!c->ssl || !c->rbio || !c->wbio)
+ {
+ /* Not yet attached, so SSL_free would not release them. */
+ if (c->rbio)
+ BIO_free(c->rbio);
+ if (c->wbio)
+ BIO_free(c->wbio);
+ c->rbio = c->wbio = NULL;
+ h2c_fail(c, env, "out of memory setting up TLS");
+ return AXIS2_FAILURE;
+ }
+ SSL_set_bio(c->ssl, c->rbio, c->wbio); /* ssl owns both from here */
+ SSL_set_connect_state(c->ssl);
+
+ /* Name checks. An IP literal is matched against the certificate's IP
+ * SANs and is never sent as SNI (RFC 6066 forbids it); a DNS name is
+ * matched against its DNS SANs and sent as SNI. */
+ if (c->verify_name_is_ip)
+ {
+ if (X509_VERIFY_PARAM_set1_ip_asc(SSL_get0_param(c->ssl), c->verify_name) != 1)
+ {
+ h2c_fail(c, env, "cannot use %s as a verify name", c->verify_name);
+ return AXIS2_FAILURE;
+ }
+ }
+ else
+ {
+ SSL_set_hostflags(c->ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
+ if (SSL_set1_host(c->ssl, c->verify_name) != 1 ||
+ SSL_set_tlsext_host_name(c->ssl, c->verify_name) != 1)
+ {
+ h2c_fail(c, env, "cannot use %s as a verify name", c->verify_name);
+ return AXIS2_FAILURE;
+ }
+ }
+
+ for (;;)
+ {
+ long left;
+ rv = SSL_do_handshake(c->ssl);
+ if (h2c_flush(c, env) != AXIS2_SUCCESS)
+ return AXIS2_FAILURE;
+ if (rv == 1)
+ break;
+ if (SSL_get_error(c->ssl, rv) != SSL_ERROR_WANT_READ)
+ {
+ long vr = SSL_get_verify_result(c->ssl);
+ if (vr != X509_V_OK)
+ h2c_fail(c, env, "server certificate rejected (verify name %s): %s",
+ c->verify_name, X509_verify_cert_error_string(vr));
+ else
+ h2c_fail(c, env, "TLS handshake failed: %s", h2c_ssl_reason(why, sizeof(why)));
+ return AXIS2_FAILURE;
+ }
+ left = deadline - h2c_now_ms();
+ if (left <= 0)
+ {
+ h2c_fail(c, env, "TLS handshake timed out after %d ms", c->connect_timeout_ms);
+ return AXIS2_FAILURE;
+ }
+ if (h2c_fill(c, env, (int)left) != AXIS2_SUCCESS)
+ return AXIS2_FAILURE;
+ }
+
+ SSL_get0_alpn_selected(c->ssl, &alpn, &alpn_len);
+ if (alpn_len != 2 || memcmp(alpn, "h2", 2) != 0)
+ {
+ h2c_fail(c, env, "the server did not negotiate HTTP/2 (ALPN h2); "
+ "Axis2/C serves JSON over HTTP/2 only");
+ return AXIS2_FAILURE;
+ }
+
+ if (nghttp2_session_callbacks_new(&cbs) != 0)
+ {
+ h2c_fail(c, env, "out of memory setting up HTTP/2");
+ return AXIS2_FAILURE;
+ }
+ nghttp2_session_callbacks_set_on_header_callback(cbs, h2c_on_header);
+ nghttp2_session_callbacks_set_on_data_chunk_recv_callback(cbs, h2c_on_data);
+ nghttp2_session_callbacks_set_on_stream_close_callback(cbs, h2c_on_stream_close);
+ rv = nghttp2_session_client_new(&c->session, cbs, c);
+ nghttp2_session_callbacks_del(cbs);
+ if (rv != 0)
+ {
+ h2c_fail(c, env, "cannot create the HTTP/2 session: %s", nghttp2_strerror(rv));
+ return AXIS2_FAILURE;
+ }
+
+ settings[0].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH;
+ settings[0].value = 0;
+ settings[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE;
+ settings[1].value = H2C_STREAM_WINDOW;
+ rv = nghttp2_submit_settings(c->session, NGHTTP2_FLAG_NONE, settings, 2);
+ if (rv == 0)
+ rv = nghttp2_session_set_local_window_size(c->session, NGHTTP2_FLAG_NONE, 0,
+ H2C_CONNECTION_WINDOW);
+ if (rv != 0)
+ {
+ h2c_fail(c, env, "cannot send HTTP/2 settings: %s", nghttp2_strerror(rv));
+ return AXIS2_FAILURE;
+ }
+ AXIS2_LOG_DEBUG(env->log, AXIS2_LOG_SI, "[h2_json_client] connected to %s:%d (%s, %s)",
+ c->host, c->port, SSL_get_version(c->ssl), c->verify_name);
+ return AXIS2_SUCCESS;
+}
+
+/* ------------------------------------------------------------------------ */
+/* public API */
+/* ------------------------------------------------------------------------ */
+
+static int
+h2c_is_ip_literal(const char *s)
+{
+ unsigned char buf[sizeof(struct in6_addr)];
+ return inet_pton(AF_INET, s, buf) == 1 || inet_pton(AF_INET6, s, buf) == 1;
+}
+
+AXIS2_EXTERN axis2_h2_json_client_t *AXIS2_CALL
+axis2_h2_json_client_create(
+ const axutil_env_t *env,
+ const axis2_h2_json_client_options_t *o)
+{
+ axis2_h2_json_client_t *c;
+ static const unsigned char alpn_h2[] = { 2, 'h', '2' };
+ char why[160];
+ const char *missing = NULL;
+ size_t alen;
+
+ if (!env)
+ return NULL;
+ if (!o)
+ missing = "options";
+ else if (!o->host || !o->host[0])
+ missing = "host";
+ else if (o->port <= 0 || o->port > 65535)
+ missing = "port (1-65535)";
+ else if (!o->ca_file || !o->ca_file[0])
+ missing = "ca_file";
+ else if (o->cert_file && (!o->key_file || !o->key_file[0]))
+ missing = "key_file (cert_file is set)";
+ if (missing)
+ {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "[h2_json_client] missing or invalid option: %s", missing);
+ return NULL;
+ }
+
+ c = AXIS2_MALLOC(env->allocator, sizeof(*c));
+ if (!c)
+ return NULL;
+ memset(c, 0, sizeof(*c));
+ c->fd = -1;
+ c->port = o->port;
+ c->connect_timeout_ms = o->connect_timeout_ms > 0 ? o->connect_timeout_ms : H2C_DEFAULT_CONNECT_MS;
+ c->io_timeout_ms = o->io_timeout_ms > 0 ? o->io_timeout_ms : H2C_DEFAULT_IO_MS;
+ c->max_response = o->max_response_bytes > 0 ? o->max_response_bytes : H2C_DEFAULT_MAX_RESPONSE;
+ c->host = axutil_strdup(env, o->host);
+ c->verify_name = axutil_strdup(env, (o->verify_name && o->verify_name[0]) ? o->verify_name : o->host);
+ if (!c->host || !c->verify_name)
+ goto fail;
+ c->verify_name_is_ip = h2c_is_ip_literal(c->verify_name);
+
+ /* :authority -- bracketed when an IPv6 literal. */
+ alen = strlen(c->verify_name) + 16;
+ c->authority = AXIS2_MALLOC(env->allocator, alen);
+ if (!c->authority)
+ goto fail;
+ snprintf(c->authority, alen, strchr(c->verify_name, ':') ? "[%s]:%d" : "%s:%d",
+ c->verify_name, c->port);
+
+ ERR_clear_error();
+ c->ctx = SSL_CTX_new(TLS_client_method());
+ if (!c->ctx)
+ {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "[h2_json_client] SSL_CTX_new: %s",
+ h2c_ssl_reason(why, sizeof(why)));
+ goto fail;
+ }
+ /* HTTP/2 requires TLS 1.2 or later (RFC 9113 section 9.2) and forbids
+ * compression and renegotiation. */
+ SSL_CTX_set_min_proto_version(c->ctx, TLS1_2_VERSION);
+ SSL_CTX_set_options(c->ctx, SSL_OP_NO_COMPRESSION
+#ifdef SSL_OP_NO_RENEGOTIATION
+ | SSL_OP_NO_RENEGOTIATION
+#endif
+ );
+ SSL_CTX_set_verify(c->ctx, SSL_VERIFY_PEER, NULL);
+ if (SSL_CTX_load_verify_locations(c->ctx, o->ca_file, NULL) != 1)
+ {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "[h2_json_client] cannot load ca_file %s: %s",
+ o->ca_file, h2c_ssl_reason(why, sizeof(why)));
+ goto fail;
+ }
+ if (o->cert_file)
+ {
+ if (SSL_CTX_use_certificate_chain_file(c->ctx, o->cert_file) != 1 ||
+ SSL_CTX_use_PrivateKey_file(c->ctx, o->key_file, SSL_FILETYPE_PEM) != 1 ||
+ SSL_CTX_check_private_key(c->ctx) != 1)
+ {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "[h2_json_client] cannot use client cert %s / key %s: %s",
+ o->cert_file, o->key_file, h2c_ssl_reason(why, sizeof(why)));
+ goto fail;
+ }
+ }
+ /* SSL_CTX_set_alpn_protos returns 0 on success. */
+ if (SSL_CTX_set_alpn_protos(c->ctx, alpn_h2, sizeof(alpn_h2)) != 0)
+ {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "[h2_json_client] cannot offer ALPN h2");
+ goto fail;
+ }
+ return c;
+
+fail:
+ axis2_h2_json_client_free(c, env);
+ return NULL;
+}
+
+AXIS2_EXTERN axis2_status_t AXIS2_CALL
+axis2_h2_json_client_post(
+ axis2_h2_json_client_t *c,
+ const axutil_env_t *env,
+ const axis2_char_t *path,
+ const axis2_char_t *json,
+ size_t json_len,
+ axis2_char_t **response_out,
+ size_t *response_len_out,
+ int *http_status_out)
+{
+ char clen[24];
+ nghttp2_nv hdrs[7];
+ int32_t sid;
+
+ if (response_out)
+ *response_out = NULL;
+ if (response_len_out)
+ *response_len_out = 0;
+ if (http_status_out)
+ *http_status_out = 0;
+ if (!c || !env)
+ return AXIS2_FAILURE;
+ c->error[0] = '\0';
+ if (!path || path[0] != '/' || !response_out || (!json && json_len > 0))
+ {
+ h2c_fail(c, env, "invalid arguments: path must start with '/', response_out is required");
+ return AXIS2_FAILURE;
+ }
+
+ /* A session the server has finished with (GOAWAY) cannot carry a new
+ * stream; open a fresh one. */
+ if (c->session && !nghttp2_session_want_read(c->session) &&
+ !nghttp2_session_want_write(c->session))
+ h2c_disconnect(c, env, 0);
+ if (!c->session && h2c_connect(c, env) != AXIS2_SUCCESS)
+ {
+ h2c_disconnect(c, env, 0);
+ return AXIS2_FAILURE;
+ }
+
+ c->env = env;
+ c->stream_closed = 0;
+ c->stream_error = 0;
+ c->http_status = 0;
+ c->req_body = json ? json : "";
+ c->req_len = json_len;
+ c->req_off = 0;
+ c->resp = NULL;
+ c->resp_len = 0;
+ c->resp_cap = 0;
+ c->resp_too_large = 0;
+
+ snprintf(clen, sizeof(clen), "%lu", (unsigned long)json_len);
+#define H2C_NV(i, n, v) do { \
+ hdrs[i].name = (uint8_t *)(n); hdrs[i].namelen = strlen(n); \
+ hdrs[i].value = (uint8_t *)(v); hdrs[i].valuelen = strlen(v); \
+ hdrs[i].flags = NGHTTP2_NV_FLAG_NONE; } while (0)
+ H2C_NV(0, ":method", "POST");
+ H2C_NV(1, ":scheme", "https");
+ H2C_NV(2, ":authority", c->authority);
+ H2C_NV(3, ":path", path);
+ H2C_NV(4, "content-type", "application/json");
+ H2C_NV(5, "accept", "application/json");
+ H2C_NV(6, "content-length", clen);
+#undef H2C_NV
+
+ {
+#ifdef H2C_NGHTTP2_V2
+ nghttp2_data_provider2 body;
+ body.source.ptr = NULL;
+ body.read_callback = h2c_read_body;
+ sid = nghttp2_submit_request2(c->session, NULL, hdrs, 7, &body, c);
+#else
+ nghttp2_data_provider body;
+ body.source.ptr = NULL;
+ body.read_callback = h2c_read_body;
+ sid = nghttp2_submit_request(c->session, NULL, hdrs, 7, &body, c);
+#endif
+ }
+ if (sid < 0)
+ {
+ h2c_fail(c, env, "cannot submit the request: %s", nghttp2_strerror(sid));
+ goto fail;
+ }
+ c->stream_id = sid;
+
+ while (!c->stream_closed)
+ {
+ if (h2c_pump_send(c, env) != AXIS2_SUCCESS)
+ goto fail;
+ if (c->stream_closed)
+ break;
+ if (!nghttp2_session_want_read(c->session) && !nghttp2_session_want_write(c->session))
+ {
+ h2c_fail(c, env, "the server ended the HTTP/2 session before responding");
+ goto fail;
+ }
+ if (h2c_pump_recv(c, env) != AXIS2_SUCCESS)
+ goto fail;
+ }
+ /* Acknowledge what arrived with the response (settings, window updates). */
+ if (h2c_pump_send(c, env) != AXIS2_SUCCESS)
+ goto fail;
+
+ if (c->resp_too_large)
+ {
+ h2c_fail(c, env, "response to %s exceeded the %lu-byte limit", path,
+ (unsigned long)c->max_response);
+ goto fail;
+ }
+ if (c->stream_error != NGHTTP2_NO_ERROR)
+ {
+ h2c_fail(c, env, "the server reset the stream for %s: %s", path,
+ nghttp2_http2_strerror(c->stream_error));
+ goto fail;
+ }
+ if (c->http_status == 0)
+ {
+ h2c_fail(c, env, "the response to %s carried no :status", path);
+ goto fail;
+ }
+
+ if (!c->resp)
+ {
+ c->resp = AXIS2_MALLOC(env->allocator, 1);
+ if (!c->resp)
+ {
+ h2c_fail(c, env, "out of memory");
+ goto fail;
+ }
+ }
+ c->resp[c->resp_len] = '\0';
+ *response_out = c->resp;
+ if (response_len_out)
+ *response_len_out = c->resp_len;
+ if (http_status_out)
+ *http_status_out = c->http_status;
+ c->resp = NULL;
+ c->req_body = NULL;
+ return AXIS2_SUCCESS;
+
+fail:
+ if (c->resp)
+ AXIS2_FREE(env->allocator, c->resp);
+ c->resp = NULL;
+ c->req_body = NULL;
+ h2c_disconnect(c, env, 0);
+ return AXIS2_FAILURE;
+}
+
+AXIS2_EXTERN const axis2_char_t *AXIS2_CALL
+axis2_h2_json_client_get_error(const axis2_h2_json_client_t *c)
+{
+ return c ? c->error : "";
+}
+
+AXIS2_EXTERN void AXIS2_CALL
+axis2_h2_json_client_free(axis2_h2_json_client_t *c, const axutil_env_t *env)
+{
+ if (!c || !env)
+ return;
+ h2c_disconnect(c, env, 1);
+ if (c->ctx)
+ SSL_CTX_free(c->ctx);
+ if (c->host)
+ AXIS2_FREE(env->allocator, c->host);
+ if (c->verify_name)
+ AXIS2_FREE(env->allocator, c->verify_name);
+ if (c->authority)
+ AXIS2_FREE(env->allocator, c->authority);
+ AXIS2_FREE(env->allocator, c);
+}
diff --git a/src/core/transport/h2/test/Makefile.am b/src/core/transport/h2/test/Makefile.am
index d7191675d..30642c5f2 100644
--- a/src/core/transport/h2/test/Makefile.am
+++ b/src/core/transport/h2/test/Makefile.am
@@ -19,9 +19,9 @@
# These tests focus exclusively on HTTP/2 JSON processing without SOAP dependencies
if WITH_NGHTTP2
-check_PROGRAMS = h2_json_integration_test h2_performance_benchmark_test
-noinst_PROGRAMS = h2_json_integration_test h2_performance_benchmark_test
-TESTS = h2_json_integration_test h2_performance_benchmark_test
+check_PROGRAMS = h2_json_integration_test h2_performance_benchmark_test h2_json_client_test
+noinst_PROGRAMS = h2_json_integration_test h2_performance_benchmark_test h2_json_client_test
+TESTS = h2_json_integration_test h2_performance_benchmark_test h2_json_client_test
# ASAN preload for libtool-wrapped test execution
if ENABLE_ASAN
@@ -66,6 +66,16 @@ h2_performance_benchmark_test_CPPFLAGS = \
$(JSON_CFLAGS) \
-DAXIS2_H2_PERFORMANCE_TESTS
+# HTTP/2 JSON client. Offline checks always run; live checks need a server
+# named by AXIS2_H2_TEST_HOST/_PORT/_CA (see the file header).
+h2_json_client_test_SOURCES = h2_json_client_test.c
+h2_json_client_test_LDADD = \
+ $(top_builddir)/src/core/transport/h2/sender/libaxis2_h2_client.la \
+ $(top_builddir)/util/src/libaxutil.la
+h2_json_client_test_CPPFLAGS = \
+ -I$(top_srcdir)/include \
+ -I$(top_srcdir)/util/include
+
# Test data files
EXTRA_DIST = large_json_payload_generator.h
diff --git a/src/core/transport/h2/test/h2_json_client_test.c b/src/core/transport/h2/test/h2_json_client_test.c
new file mode 100644
index 000000000..03eedd3bf
--- /dev/null
+++ b/src/core/transport/h2/test/h2_json_client_test.c
@@ -0,0 +1,217 @@
+/*
+ * 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.
+ */
+
+/*
+ * Tests for the HTTP/2 JSON client (axis2_h2_json_client.h).
+ *
+ * The offline checks always run. The live checks need a real Axis2/C server
+ * speaking JSON over HTTP/2 with TLS, and run only when it is named:
+ *
+ * AXIS2_H2_TEST_HOST address to connect to
+ * AXIS2_H2_TEST_PORT port
+ * AXIS2_H2_TEST_CA PEM CA the server certificate chains to
+ * AXIS2_H2_TEST_CERT/_KEY client certificate and key, if the server wants mTLS
+ * AXIS2_H2_TEST_NAME name the server certificate carries, if not HOST
+ * AXIS2_H2_TEST_PATH operation path (default: the financial benchmark
+ * sample's portfolioVariance)
+ */
+
+#include <axis2_h2_json_client.h>
+#include <axutil_env.h>
+#include <axutil_error_default.h>
+#include <axutil_log_default.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static int failures = 0;
+
+#define CHECK(cond, what) do { \
+ if (cond) printf(" ok %s\n", what); \
+ else { printf(" FAIL %s\n", what); failures++; } } while (0)
+
+static const char *PV_BODY =
+ "{\"n_assets\":2,\"weights\":[0.5,0.5],"
+ "\"covariance_matrix\":[[0.04,0.01],[0.01,0.09]],\"n_periods_per_year\":1}";
+
+static void
+offline_checks(const axutil_env_t *env)
+{
+ axis2_h2_json_client_options_t o;
+ axis2_h2_json_client_t *c;
+ axis2_char_t *resp = (axis2_char_t *)"sentinel";
+ int status = -1;
+
+ printf("offline\n");
+
+ memset(&o, 0, sizeof(o));
+ o.port = 443;
+ o.ca_file = "/nonexistent-ca.pem";
+ CHECK(axis2_h2_json_client_create(env, &o) == NULL, "refuses a missing host");
+
+ o.host = "127.0.0.1";
+ o.port = 0;
+ CHECK(axis2_h2_json_client_create(env, &o) == NULL, "refuses port 0");
+
+ o.port = 443;
+ o.ca_file = NULL;
+ CHECK(axis2_h2_json_client_create(env, &o) == NULL, "refuses a missing ca_file");
+
+ o.ca_file = "/nonexistent-ca.pem";
+ CHECK(axis2_h2_json_client_create(env, &o) == NULL, "refuses a ca_file it cannot load");
+
+ o.cert_file = "/nonexistent-cert.pem";
+ CHECK(axis2_h2_json_client_create(env, &o) == NULL, "refuses cert_file without key_file");
+ o.cert_file = NULL;
+
+ CHECK(axis2_h2_json_client_create(env, NULL) == NULL, "refuses NULL options");
+ axis2_h2_json_client_free(NULL, env);
+ CHECK(1, "free(NULL) is harmless");
+
+ /* A connect failure must be a clean FAILURE with a reason, not a hang. */
+ {
+ const char *ca = getenv("AXIS2_H2_TEST_CA");
+ if (ca)
+ {
+ o.ca_file = ca;
+ o.host = "127.0.0.1";
+ o.port = 1; /* nothing listens on tcpmux */
+ o.connect_timeout_ms = 2000;
+ c = axis2_h2_json_client_create(env, &o);
+ CHECK(c != NULL, "creates with a loadable CA");
+ if (c)
+ {
+ axis2_status_t s = axis2_h2_json_client_post(c, env, "/services/X/op",
+ "{}", 2, &resp, NULL, &status);
+ CHECK(s == AXIS2_FAILURE, "a refused connection fails");
+ CHECK(resp == NULL && status == 0, "and clears the outputs");
+ CHECK(strstr(axis2_h2_json_client_get_error(c), "cannot connect") != NULL,
+ "and says why");
+ s = axis2_h2_json_client_post(c, env, "no-slash", "{}", 2, &resp, NULL, NULL);
+ CHECK(s == AXIS2_FAILURE, "refuses a path without a leading '/'");
+ axis2_h2_json_client_free(c, env);
+ }
+ }
+ else
+ printf(" skip connect-failure checks (set AXIS2_H2_TEST_CA)\n");
+ }
+}
+
+static void
+fill_live_options(axis2_h2_json_client_options_t *o)
+{
+ memset(o, 0, sizeof(*o));
+ o->host = getenv("AXIS2_H2_TEST_HOST");
+ o->port = atoi(getenv("AXIS2_H2_TEST_PORT"));
+ o->ca_file = getenv("AXIS2_H2_TEST_CA");
+ o->cert_file = getenv("AXIS2_H2_TEST_CERT");
+ o->key_file = getenv("AXIS2_H2_TEST_KEY");
+ o->verify_name = getenv("AXIS2_H2_TEST_NAME");
+}
+
+static void
+live_checks(const axutil_env_t *env)
+{
+ axis2_h2_json_client_options_t o;
+ axis2_h2_json_client_t *c;
+ const char *path = getenv("AXIS2_H2_TEST_PATH");
+ axis2_char_t *resp = NULL;
+ size_t len = 0;
+ int status = 0, i;
+
+ if (!getenv("AXIS2_H2_TEST_HOST") || !getenv("AXIS2_H2_TEST_PORT") ||
+ !getenv("AXIS2_H2_TEST_CA"))
+ {
+ printf("live\n skip no server named (set AXIS2_H2_TEST_HOST, _PORT, _CA)\n");
+ return;
+ }
+ if (!path)
+ path = "/services/FinancialBenchmarkService/portfolioVariance";
+ printf("live: %s:%s%s\n", getenv("AXIS2_H2_TEST_HOST"), getenv("AXIS2_H2_TEST_PORT"), path);
+
+ fill_live_options(&o);
+ c = axis2_h2_json_client_create(env, &o);
+ CHECK(c != NULL, "creates");
+ if (!c)
+ return;
+
+ /* Three posts on one client: the connection is opened once and reused. */
+ for (i = 0; i < 3; i++)
+ {
+ axis2_status_t s = axis2_h2_json_client_post(c, env, path, PV_BODY, strlen(PV_BODY),
+ &resp, &len, &status);
+ char label[64];
+ snprintf(label, sizeof(label), "post %d answers 200 with a body", i + 1);
+ CHECK(s == AXIS2_SUCCESS && status == 200 && resp && len > 0, label);
+ if (s != AXIS2_SUCCESS)
+ printf(" error: %s\n", axis2_h2_json_client_get_error(c));
+ if (resp)
+ {
+ if (i == 0)
+ printf(" %.*s\n", (int)(len < 200 ? len : 200), resp);
+ CHECK(strlen(resp) == len, "body is NUL-terminated at its length");
+ AXIS2_FREE(env->allocator, resp);
+ resp = NULL;
+ }
+ }
+
+ /* A response larger than the cap is abandoned, not truncated. */
+ axis2_h2_json_client_free(c, env);
+ fill_live_options(&o);
+ o.max_response_bytes = 8;
+ c = axis2_h2_json_client_create(env, &o);
+ if (c)
+ {
+ axis2_status_t s = axis2_h2_json_client_post(c, env, path, PV_BODY, strlen(PV_BODY),
+ &resp, &len, &status);
+ CHECK(s == AXIS2_FAILURE && resp == NULL, "a response over max_response_bytes fails");
+ CHECK(strstr(axis2_h2_json_client_get_error(c), "exceeded") != NULL, "and says why");
+ /* ...and the client recovers on the next call with a sane limit. */
+ axis2_h2_json_client_free(c, env);
+ }
+
+ /* The certificate name is checked: a name the certificate does not carry
+ * is refused even though the chain is good. */
+ fill_live_options(&o);
+ o.verify_name = "not-the-server.invalid";
+ c = axis2_h2_json_client_create(env, &o);
+ if (c)
+ {
+ axis2_status_t s = axis2_h2_json_client_post(c, env, path, PV_BODY, strlen(PV_BODY),
+ &resp, &len, &status);
+ CHECK(s == AXIS2_FAILURE, "a certificate that does not carry verify_name is refused");
+ printf(" error: %s\n", axis2_h2_json_client_get_error(c));
+ axis2_h2_json_client_free(c, env);
+ }
+}
+
+int
+main(void)
+{
+ axutil_allocator_t *allocator = axutil_allocator_init(NULL);
+ axutil_error_t *error = axutil_error_create(allocator);
+ axutil_log_t *log = axutil_log_create(allocator, NULL, "h2_json_client_test.log");
+ axutil_env_t *env = axutil_env_create_with_error_log(allocator, error, log);
+
+ offline_checks(env);
+ live_checks(env);
+
+ printf("%s: %d failure(s)\n", failures ? "FAILED" : "PASSED", failures);
+ axutil_env_free(env);
+ return failures ? 1 : 0;
+}
(axis-axis2-c-core) 03/06: Bound the h2 client's handshake writes by connect_timeout_ms
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit cac64816b243a2829874c9c1f69db8deb27c850e
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Thu Sep 24 13:22:28 2026 -1000
Bound the h2 client's handshake writes by connect_timeout_ms
A handshake write that blocked waited io_timeout_ms, not the remaining
connect deadline. Also: monotonic time is 64-bit (a 32-bit long of ms
wraps after 24.8 days), response-buffer doubling cannot overflow size_t,
and the file gains comments on how bytes flow through TLS and nghttp2.
Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
.../transport/h2/sender/axis2_h2_json_client.c | 140 ++++++++++++++++-----
1 file changed, 108 insertions(+), 32 deletions(-)
diff --git a/src/core/transport/h2/sender/axis2_h2_json_client.c b/src/core/transport/h2/sender/axis2_h2_json_client.c
index 2a5734347..535f812ab 100644
--- a/src/core/transport/h2/sender/axis2_h2_json_client.c
+++ b/src/core/transport/h2/sender/axis2_h2_json_client.c
@@ -23,6 +23,18 @@
* The memory BIOs keep every byte of socket I/O in this file, so each wait is
* a poll() with a deadline, a write to a closed peer cannot raise SIGPIPE
* (send() with MSG_NOSIGNAL), and nothing ever blocks without a timeout.
+ *
+ * Bytes move in two directions, and every function below is one step:
+ *
+ * out: nghttp2_session_mem_send -> SSL_write -> wbio -> h2c_flush -> send()
+ * (h2c_pump_send drives the whole chain)
+ * in: recv() -> h2c_fill -> rbio -> SSL_read -> nghttp2_session_mem_recv
+ * -> the h2c_on_* callbacks (h2c_pump_recv drives the whole chain)
+ *
+ * OpenSSL never touches the socket: it reads ciphertext from rbio and writes
+ * ciphertext to wbio, and this file moves it between the BIOs and the fd.
+ * nghttp2 likewise never touches TLS: it hands out and takes in plaintext
+ * frames. One request is in flight at a time; its state lives in the struct.
*/
#include <axis2_h2_json_client.h>
@@ -45,6 +57,7 @@
#include <unistd.h>
#include <errno.h>
#include <stdarg.h>
+#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
@@ -90,19 +103,20 @@ struct axis2_h2_json_client
BIO *wbio; /* TLS -> network; owned by ssl */
nghttp2_session *session;
- /* the request in flight */
+ /* the request in flight -- written by the nghttp2 callbacks, read by
+ * axis2_h2_json_client_post once the stream closes */
const axutil_env_t *env;
int32_t stream_id;
- int stream_closed;
- uint32_t stream_error;
- int http_status;
- const axis2_char_t *req_body;
+ int stream_closed; /* set by h2c_on_stream_close */
+ uint32_t stream_error; /* RST_STREAM code; NGHTTP2_NO_ERROR if clean */
+ int http_status; /* from :status; 0 until the headers arrive */
+ const axis2_char_t *req_body; /* caller's buffer, not copied */
size_t req_len;
- size_t req_off;
- axis2_char_t *resp;
+ size_t req_off; /* how much of req_body nghttp2 has taken */
+ axis2_char_t *resp; /* grows in h2c_on_data; handed to the caller */
size_t resp_len;
size_t resp_cap;
- int resp_too_large;
+ int resp_too_large; /* max_response hit; the stream was cancelled */
axis2_char_t error[256];
};
@@ -135,12 +149,14 @@ h2c_ssl_reason(char *buf, size_t len)
return buf;
}
-static long
+/* 64-bit so the arithmetic cannot overflow where long is 32 bits: a 32-bit
+ * long of milliseconds wraps after 24.8 days of uptime. */
+static int64_t
h2c_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
- return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+ return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* poll() one descriptor, retrying on EINTR against the same deadline.
@@ -148,11 +164,11 @@ h2c_now_ms(void)
static int
h2c_wait(int fd, short events, int timeout_ms)
{
- long deadline = h2c_now_ms() + timeout_ms;
+ int64_t deadline = h2c_now_ms() + timeout_ms;
for (;;)
{
struct pollfd p;
- long left = deadline - h2c_now_ms();
+ int64_t left = deadline - h2c_now_ms();
int r;
if (left < 0)
left = 0;
@@ -173,9 +189,15 @@ h2c_wait(int fd, short events, int timeout_ms)
/* socket I/O under the memory BIOs */
/* ------------------------------------------------------------------------ */
+/* deadline for h2c_send_all / h2c_flush: none, so each wait for the socket
+ * to drain gets io_timeout_ms. The handshake passes its connect deadline
+ * instead, so a server that stalls its receive window cannot stretch
+ * connect_timeout_ms to io_timeout_ms. */
+#define H2C_NO_DEADLINE ((int64_t)-1)
+
static axis2_status_t
h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
- const char *buf, size_t len)
+ const char *buf, size_t len, int64_t deadline)
{
while (len > 0)
{
@@ -190,7 +212,9 @@ h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
continue;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
{
- int w = h2c_wait(c->fd, POLLOUT, c->io_timeout_ms);
+ int64_t left = deadline == H2C_NO_DEADLINE ? c->io_timeout_ms
+ : deadline - h2c_now_ms();
+ int w = left > 0 ? h2c_wait(c->fd, POLLOUT, (int)left) : 0;
if (w > 0)
continue;
h2c_fail(c, env, w == 0 ? "timed out sending to the server"
@@ -203,21 +227,26 @@ h2c_send_all(axis2_h2_json_client_t *c, const axutil_env_t *env,
return AXIS2_SUCCESS;
}
-/* Send whatever TLS has produced. */
+/* Send whatever TLS has produced -- the ciphertext waiting in wbio. Called
+ * after anything that can make OpenSSL write: a handshake step, SSL_write,
+ * SSL_shutdown, and SSL_read (which can owe the peer a reply). */
static axis2_status_t
-h2c_flush(axis2_h2_json_client_t *c, const axutil_env_t *env)
+h2c_flush(axis2_h2_json_client_t *c, const axutil_env_t *env, int64_t deadline)
{
char buf[H2C_IO_CHUNK];
int n;
while ((n = BIO_read(c->wbio, buf, (int)sizeof(buf))) > 0)
{
- if (h2c_send_all(c, env, buf, (size_t)n) != AXIS2_SUCCESS)
+ if (h2c_send_all(c, env, buf, (size_t)n, deadline) != AXIS2_SUCCESS)
return AXIS2_FAILURE;
}
return AXIS2_SUCCESS;
}
-/* Wait up to timeout_ms for bytes from the server and hand them to TLS. */
+/* Wait up to timeout_ms for bytes from the server and hand them to TLS.
+ * One recv() per call: the caller asks OpenSSL for plaintext afterwards and
+ * calls again only if OpenSSL wants more. timeout_ms 0 is a non-blocking
+ * check (see h2c_peer_gone). */
static axis2_status_t
h2c_fill(axis2_h2_json_client_t *c, const axutil_env_t *env, int timeout_ms)
{
@@ -261,6 +290,12 @@ h2c_fill(axis2_h2_json_client_t *c, const axutil_env_t *env, int timeout_ms)
/* nghttp2 callbacks */
/* ------------------------------------------------------------------------ */
+/* nghttp2 calls the h2c_on_* callbacks from inside nghttp2_session_mem_recv,
+ * i.e. from h2c_pump_recv. They only record into the struct; frames for any
+ * stream other than the request in flight are ignored. */
+
+/* Keep :status. A valid status is exactly three digits (RFC 9110 15);
+ * anything else leaves http_status 0, which the post reports. */
static int
h2c_on_header(nghttp2_session *session, const nghttp2_frame *frame,
const uint8_t *name, size_t namelen,
@@ -286,9 +321,14 @@ h2c_on_data(nghttp2_session *session, uint8_t flags, int32_t stream_id,
(void)flags;
if (stream_id != c->stream_id || c->resp_too_large)
return 0;
+ /* Written as a subtraction so it cannot overflow; resp_len never exceeds
+ * max_response, so the right-hand side cannot underflow. */
if (len > c->max_response - c->resp_len)
{
- /* Stop the server rather than keep buffering; the post reports it. */
+ /* Stop the server rather than keep buffering; the post reports it.
+ * Returning 0 rather than an error keeps the session parseable until
+ * the stream closes, so the failure is reported as "too large" and
+ * not as a protocol error. */
c->resp_too_large = 1;
nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_CANCEL);
return 0;
@@ -297,8 +337,11 @@ h2c_on_data(nghttp2_session *session, uint8_t flags, int32_t stream_id,
{
size_t cap = c->resp_cap ? c->resp_cap : 4096;
axis2_char_t *grown;
- while (cap < c->resp_len + len + 1)
- cap *= 2;
+ /* need cannot wrap: resp_len + len <= max_response < SIZE_MAX (see
+ * create). Doubling stops short of overflow and takes need instead. */
+ size_t need = c->resp_len + len + 1;
+ while (cap < need)
+ cap = cap > SIZE_MAX / 2 ? need : cap * 2;
grown = AXIS2_REALLOC(c->env->allocator, c->resp, cap);
if (!grown)
return NGHTTP2_ERR_CALLBACK_FAILURE;
@@ -324,6 +367,8 @@ h2c_on_stream_close(nghttp2_session *session, int32_t stream_id,
return 0;
}
+/* nghttp2 pulls the request body through this as flow control allows,
+ * `length` bytes at most per call; EOF is flagged with the last chunk. */
#ifdef H2C_NGHTTP2_V2
static nghttp2_ssize
h2c_read_body(nghttp2_session *session, int32_t stream_id, uint8_t *buf,
@@ -380,11 +425,16 @@ h2c_pump_send(axis2_h2_json_client_t *c, const axutil_env_t *env)
return AXIS2_FAILURE;
}
}
- return h2c_flush(c, env);
+ return h2c_flush(c, env, H2C_NO_DEADLINE);
}
/* Read what the server has sent -- waiting up to io_timeout for the first
- * byte -- and feed it to nghttp2. Returns once TLS has nothing more to give. */
+ * byte -- and feed it to nghttp2. Returns once TLS has nothing more to give.
+ *
+ * SSL_read returning WANT_READ means "no complete record buffered". Before
+ * anything has been decoded that means wait on the socket; after something
+ * has, return so the caller can act on it (send WINDOW_UPDATEs, see whether
+ * the stream closed) instead of blocking for more. */
static axis2_status_t
h2c_pump_recv(axis2_h2_json_client_t *c, const axutil_env_t *env)
{
@@ -415,7 +465,7 @@ h2c_pump_recv(axis2_h2_json_client_t *c, const axutil_env_t *env)
if (got_any)
return AXIS2_SUCCESS;
/* TLS may owe the server something first (a key update). */
- if (h2c_flush(c, env) != AXIS2_SUCCESS ||
+ if (h2c_flush(c, env, H2C_NO_DEADLINE) != AXIS2_SUCCESS ||
h2c_fill(c, env, c->io_timeout_ms) != AXIS2_SUCCESS)
return AXIS2_FAILURE;
break;
@@ -441,12 +491,15 @@ h2c_disconnect(axis2_h2_json_client_t *c, const axutil_env_t *env, int graceful)
{
if (graceful && c->session && c->ssl)
{
- /* Best effort: GOAWAY and close_notify, without waiting for replies. */
+ /* Best effort: GOAWAY and close_notify, without waiting for replies.
+ * Only on free(); after a failure the connection is dropped outright.
+ * c->error is saved and restored so a failure here cannot overwrite
+ * the error the caller is about to read. */
axis2_char_t saved[sizeof(c->error)];
memcpy(saved, c->error, sizeof(saved));
nghttp2_session_terminate_session(c->session, NGHTTP2_NO_ERROR);
if (h2c_pump_send(c, env) == AXIS2_SUCCESS && SSL_shutdown(c->ssl) >= 0)
- h2c_flush(c, env);
+ h2c_flush(c, env, H2C_NO_DEADLINE);
memcpy(c->error, saved, sizeof(saved));
ERR_clear_error();
}
@@ -469,8 +522,12 @@ h2c_disconnect(axis2_h2_json_client_t *c, const axutil_env_t *env, int graceful)
}
}
+/* Try each address getaddrinfo returns (IPv6 and IPv4, in its order) until
+ * one connects. The socket is non-blocking from the start so the connect
+ * itself honours the deadline; it stays non-blocking for the connection's
+ * life, which is what lets every later wait go through h2c_wait. */
static axis2_status_t
-h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, long deadline)
+h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, int64_t deadline)
{
struct addrinfo hints, *res = NULL, *ai;
char port[16];
@@ -492,7 +549,7 @@ h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, long deadlin
int fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
int one = 1, err = 0;
socklen_t errlen = sizeof(err);
- long left;
+ int64_t left;
if (fd < 0)
{
last_errno = errno;
@@ -535,7 +592,7 @@ h2c_tcp_connect(axis2_h2_json_client_t *c, const axutil_env_t *env, long deadlin
static axis2_status_t
h2c_connect(axis2_h2_json_client_t *c, const axutil_env_t *env)
{
- long deadline = h2c_now_ms() + c->connect_timeout_ms;
+ int64_t deadline = h2c_now_ms() + c->connect_timeout_ms;
const unsigned char *alpn = NULL;
unsigned int alpn_len = 0;
nghttp2_session_callbacks *cbs = NULL;
@@ -585,11 +642,15 @@ h2c_connect(axis2_h2_json_client_t *c, const axutil_env_t *env)
}
}
+ /* Handshake: each SSL_do_handshake step may leave records in wbio (sent
+ * now) and then want the server's reply (waited for against the same
+ * deadline as the TCP connect). Certificate and name verification
+ * happen inside the handshake, so a failure here names the reason. */
for (;;)
{
- long left;
+ int64_t left;
rv = SSL_do_handshake(c->ssl);
- if (h2c_flush(c, env) != AXIS2_SUCCESS)
+ if (h2c_flush(c, env, deadline) != AXIS2_SUCCESS)
return AXIS2_FAILURE;
if (rv == 1)
break;
@@ -637,6 +698,10 @@ h2c_connect(axis2_h2_json_client_t *c, const axutil_env_t *env)
return AXIS2_FAILURE;
}
+ /* No server push (nothing here could use it). Larger receive windows
+ * than the 64 KiB default so a big response is not throttled to one
+ * window per round trip; max_response still bounds what is kept. The
+ * SETTINGS frame is only queued here -- the first post sends it. */
settings[0].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH;
settings[0].value = 0;
settings[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE;
@@ -757,13 +822,18 @@ axis2_h2_json_client_create(
c->connect_timeout_ms = o->connect_timeout_ms > 0 ? o->connect_timeout_ms : H2C_DEFAULT_CONNECT_MS;
c->io_timeout_ms = o->io_timeout_ms > 0 ? o->io_timeout_ms : H2C_DEFAULT_IO_MS;
c->max_response = o->max_response_bytes > 0 ? o->max_response_bytes : H2C_DEFAULT_MAX_RESPONSE;
+ if (c->max_response == SIZE_MAX)
+ c->max_response = SIZE_MAX - 1; /* room for the terminating NUL */
c->host = axutil_strdup(env, o->host);
c->verify_name = axutil_strdup(env, (o->verify_name && o->verify_name[0]) ? o->verify_name : o->host);
if (!c->host || !c->verify_name)
goto fail;
c->verify_name_is_ip = h2c_is_ip_literal(c->verify_name);
- /* :authority -- bracketed when an IPv6 literal. */
+ /* :authority -- bracketed when an IPv6 literal. The name the certificate
+ * is checked against is also the one the request names, so a virtual
+ * host behind a load balancer's address routes correctly. +16 covers
+ * "[", "]", ":", five port digits and the NUL. */
alen = strlen(c->verify_name) + 16;
c->authority = AXIS2_MALLOC(env->allocator, alen);
if (!c->authority)
@@ -912,6 +982,10 @@ axis2_h2_json_client_post(
}
c->stream_id = sid;
+ /* Alternate: send what nghttp2 has queued (headers, body as the window
+ * opens, WINDOW_UPDATEs, SETTINGS ACKs), then wait for the server,
+ * until the callbacks mark the stream closed. If the session wants
+ * neither to read nor write, the server has GOAWAYed us mid-request. */
while (!c->stream_closed)
{
if (h2c_pump_send(c, env) != AXIS2_SUCCESS)
@@ -957,6 +1031,8 @@ axis2_h2_json_client_post(
goto fail;
}
}
+ /* The terminator is not counted in resp_len; h2c_on_data always leaves
+ * room for it. Ownership of the buffer passes to the caller. */
c->resp[c->resp_len] = '\0';
*response_out = c->resp;
if (response_len_out)
(axis-axis2-c-core) 06/06: Note that a long-running h2 client call needs both timeouts raised
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-c-core.git
commit a0c52b3b0c5f9995cdc4fd8d1e42a8d6fb7053a8
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Thu Sep 24 15:38:28 2026 -1000
Note that a long-running h2 client call needs both timeouts raised
Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
docs/HTTP2_JSON_CLIENT.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/HTTP2_JSON_CLIENT.md b/docs/HTTP2_JSON_CLIENT.md
index 3403e61f8..94f46e99b 100644
--- a/docs/HTTP2_JSON_CLIENT.md
+++ b/docs/HTTP2_JSON_CLIENT.md
@@ -49,6 +49,12 @@ axis2_h2_json_client_free(c, env);
One client holds one connection, opened on the first post and reused by the
next. A client is not thread-safe; use one per thread.
+An operation that works for a long time before it answers -- one that
+starts a file transfer and replies when it finishes, say -- sends nothing on
+the stream meanwhile, so it needs both `io_timeout_ms` and
+`request_timeout_ms` raised to its longest expected run. Set them on a client
+kept for that operation; other clients keep the short defaults.
+
## Rules it enforces
| Rule | Why |