Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions docs/httpcalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading