From 783b932b51d64b4c1c39ce3487c62d45fd337756 Mon Sep 17 00:00:00 2001 From: Gregor Jarisch Date: Mon, 5 Oct 2026 10:36:48 +0200 Subject: [PATCH] test(httpclient): pin that the shared httpcalls client keeps no cookie jar The shared client was moved off WebClientSession in #862 but nothing proved that a Set-Cookie is not replayed across calls. Adds a loopback regression test, documents the behaviour and fixes a stale test comment. --- ...10-05-fix-httpclient-no-cookie-jar-test.md | 10 +++ docs/httpcalls.md | 9 +++ .../bootstrap/HttpClientModuleCookieTest.java | 74 +++++++++++++++++++ .../ApiCallExecutorBranchCoverageTest.java | 8 +- 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 docs/changelog.d/2026-10-05-fix-httpclient-no-cookie-jar-test.md create mode 100644 src/test/java/ai/labs/eddi/engine/httpclient/bootstrap/HttpClientModuleCookieTest.java diff --git a/docs/changelog.d/2026-10-05-fix-httpclient-no-cookie-jar-test.md b/docs/changelog.d/2026-10-05-fix-httpclient-no-cookie-jar-test.md new file mode 100644 index 0000000000..bfdf671752 --- /dev/null +++ b/docs/changelog.d/2026-10-05-fix-httpclient-no-cookie-jar-test.md @@ -0,0 +1,10 @@ +## Regression test: outbound httpcalls client keeps no cookie jar (2026-10-05) + +**Repo/branch:** EDDI, `fix/httpclient-no-cookie-jar` + +**What changed** +- The shared Vert.x client behind every agent's httpcalls was already switched from an application-scoped `WebClientSession` (cookie store, replayed `Set-Cookie` across agents, users and tenants) to a plain `WebClient` in #862. No production code changed here. +- Added `HttpClientModuleCookieTest`: the real producer against a loopback server proves a `Set-Cookie` from one call is not sent on the next call to the same host. Mutation-checked by restoring `WebClientSession.create(...)`, which fails the test. +- Documented the behaviour in `docs/httpcalls.md` ("Cookies are not kept between calls"). Corrected a stale comment in `ApiCallExecutorBranchCoverageTest` that still described a cookie-aware session. + +**Behaviour note:** httpcalls never persisted cookies across calls after #862; an API that relied on a session cookie between two calls must carry the credential explicitly (header or property). Nothing in the tests, `docs/agent-configs/` or `docs/httpcalls.md` relies on session cookies. diff --git a/docs/httpcalls.md b/docs/httpcalls.md index 1cf95623f5..c03e62176a 100644 --- a/docs/httpcalls.md +++ b/docs/httpcalls.md @@ -123,6 +123,15 @@ because the client is cached and a session opened with one user's token would be reused by everyone after them. See [`mcp-server.md`](mcp-server.md#calling-an-mcp-server-as-the-chatting-user). +### Cookies are not kept between calls + +The HTTP client behind httpcalls is shared by every agent, conversation and +user, so it holds no cookie jar: a `Set-Cookie` in one response is neither stored +nor sent on a later call, even to the same host. An API that needs a session +credential on a follow-up call must have it passed explicitly, as a header (for +example `Authorization`) or from a property. `Set-Cookie` is also never recorded +in conversation memory. + ### Running behind a reverse proxy The origin is taken from the inbound request as EDDI sees it. Behind a diff --git a/src/test/java/ai/labs/eddi/engine/httpclient/bootstrap/HttpClientModuleCookieTest.java b/src/test/java/ai/labs/eddi/engine/httpclient/bootstrap/HttpClientModuleCookieTest.java new file mode 100644 index 0000000000..09e7169991 --- /dev/null +++ b/src/test/java/ai/labs/eddi/engine/httpclient/bootstrap/HttpClientModuleCookieTest.java @@ -0,0 +1,74 @@ +/* + * Copyright EDDI contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package ai.labs.eddi.engine.httpclient.bootstrap; + +import ai.labs.eddi.engine.httpclient.impl.VertxHttpClient; +import com.sun.net.httpserver.HttpServer; +import io.vertx.core.Vertx; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The outbound client behind every agent's httpcalls is shared by all agents, + * conversations and users, so it must hold no cookie jar: a {@code Set-Cookie} + * returned to one call may never be replayed on a later call to the same host. + * Runs the real producer against a real loopback server. + */ +@DisplayName("HttpClientModule does not persist cookies across calls") +class HttpClientModuleCookieTest { + + private Vertx vertx; + private HttpServer server; + private final List cookieHeaderPerRequest = new CopyOnWriteArrayList<>(); + + @BeforeEach + void setUp() throws Exception { + vertx = Vertx.vertx(); + server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); + server.createContext("/", exchange -> { + cookieHeaderPerRequest.add(String.valueOf(exchange.getRequestHeaders().getFirst("Cookie"))); + exchange.getResponseHeaders().add("Set-Cookie", "JSESSIONID=TENANT-A-SECRET; Path=/; HttpOnly"); + exchange.sendResponseHeaders(200, -1); + exchange.close(); + }); + server.start(); + } + + @AfterEach + void tearDown() throws Exception { + server.stop(0); + vertx.close().toCompletionStage().toCompletableFuture().get(10, TimeUnit.SECONDS); + } + + @Test + @DisplayName("a Set-Cookie from one request is not sent on the next request to the same host") + void setCookieIsNotReplayed() throws Exception { + HttpClientModule module = new HttpClientModule(); + module.vertx = vertx; + VertxHttpClient client = module.provideHttpClient(5, 5, 0, 5000); + + int port = server.getAddress().getPort(); + for (int i = 0; i < 2; i++) { + int status = client.getWebClient().get(port, "127.0.0.1", "/ping").send().toCompletionStage().toCompletableFuture() + .get(10, TimeUnit.SECONDS).statusCode(); + assertEquals(200, status); + } + + assertEquals(2, cookieHeaderPerRequest.size()); + assertEquals("null", cookieHeaderPerRequest.get(0), "first call has no cookie"); + assertEquals("null", cookieHeaderPerRequest.get(1), + "Set-Cookie from the first call was replayed on the second"); + } +} diff --git a/src/test/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutorBranchCoverageTest.java b/src/test/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutorBranchCoverageTest.java index 8600dbc7fb..6cf8f89bb8 100644 --- a/src/test/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutorBranchCoverageTest.java +++ b/src/test/java/ai/labs/eddi/modules/apicalls/impl/ApiCallExecutorBranchCoverageTest.java @@ -529,10 +529,10 @@ void ordersBodyBeforeHeaders() throws Exception { * choosing which headers may be STORED, and only the second one closes this. An * operation that qualifies on its documented 201 still answers some calls — the * error path especially — with a {@code Set-Cookie}, and that value is a live - * session credential: {@code HttpClientModule} builds a cookie-aware, - * application-scoped {@code WebClientSession}, so EDDI actively replays it. It - * must reach neither the tool result, the template data, nor conversation - * memory. + * session credential: the outbound client keeps no cookie jar (see + * {@code HttpClientModuleCookieTest}), but the value is still a live + * credential. It must reach neither the tool result, the template data, nor + * conversation memory. */ @Test @DisplayName("should never store or return credential-bearing response headers")