From d6fbd8b990878e52a7a451b38aeaea19a72f7e29 Mon Sep 17 00:00:00 2001 From: daan schouten Date: Sat, 17 Jan 2026 15:43:02 +0100 Subject: [PATCH 1/2] Introduce BytesReadTracker in Conjure Java --- .../java/undertow/runtime/Attachments.java | 3 + .../undertow/runtime/BytesReadTracker.java | 58 +++++++++++ .../undertow/runtime/ConjureContexts.java | 7 ++ .../runtime/BytesReadTrackerTest.java | 99 +++++++++++++++++++ .../java/undertow/lib/RequestContext.java | 6 ++ 5 files changed, 173 insertions(+) create mode 100644 conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/BytesReadTracker.java create mode 100644 conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/BytesReadTrackerTest.java diff --git a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/Attachments.java b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/Attachments.java index c675631b4..724817643 100644 --- a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/Attachments.java +++ b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/Attachments.java @@ -19,6 +19,7 @@ import com.palantir.tokens.auth.UnverifiedJsonWebToken; import io.undertow.util.AttachmentKey; import java.util.Optional; +import java.util.function.LongSupplier; public final class Attachments { @@ -27,5 +28,7 @@ public final class Attachments { public static final AttachmentKey FAILURE = AttachmentKey.create(Throwable.class); + static final AttachmentKey BYTES_READ = AttachmentKey.create(LongSupplier.class); + private Attachments() {} } diff --git a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/BytesReadTracker.java b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/BytesReadTracker.java new file mode 100644 index 000000000..61ba0a257 --- /dev/null +++ b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/BytesReadTracker.java @@ -0,0 +1,58 @@ +/* + * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + * + * Licensed 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. + */ + +package com.palantir.conjure.java.undertow.runtime; + +import com.palantir.logsafe.Preconditions; +import io.undertow.conduits.ByteActivityCallback; +import io.undertow.conduits.BytesReceivedStreamSourceConduit; +import io.undertow.server.ConduitWrapper; +import io.undertow.server.HttpServerExchange; +import java.util.function.LongSupplier; +import org.xnio.conduits.StreamSourceConduit; + +/** Tracks the number of bytes read from HTTP requests. */ +final class BytesReadTracker { + static final ConduitWrapper REQUEST_WRAPPER = (factory, exchange) -> { + BytesReadAccumulator accumulator = new BytesReadAccumulator(); + Preconditions.checkState( + exchange.putAttachment(Attachments.BYTES_READ, accumulator) == null, + "Bytes read tracker has already been registered"); + return new BytesReceivedStreamSourceConduit(factory.create(), accumulator); + }; + + static long getBytesRead(HttpServerExchange exchange) { + LongSupplier longSupplier = exchange.getAttachment(Attachments.BYTES_READ); + return longSupplier == null ? 0L : longSupplier.getAsLong(); + } + + /** Accumulates bytes read from the request and is attached to the exchange for retrieval. */ + private static final class BytesReadAccumulator implements ByteActivityCallback, LongSupplier { + private long bytesRead = 0; + + @Override + public void activity(long bytes) { + bytesRead += bytes; + } + + @Override + public long getAsLong() { + return bytesRead; + } + } + + private BytesReadTracker() {} +} diff --git a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureContexts.java b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureContexts.java index 9b2c40d95..015511bac 100644 --- a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureContexts.java +++ b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureContexts.java @@ -61,6 +61,13 @@ private static final class ConjureServerRequestContext implements RequestContext ConjureServerRequestContext(HttpServerExchange exchange, RequestArgHandler requestArgHandler) { this.exchange = exchange; this.requestArgHandler = requestArgHandler; + + exchange.addRequestWrapper(BytesReadTracker.REQUEST_WRAPPER); + } + + @Override + public long bytesRead() { + return BytesReadTracker.getBytesRead(exchange); } @Override diff --git a/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/BytesReadTrackerTest.java b/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/BytesReadTrackerTest.java new file mode 100644 index 000000000..3dce2f5a3 --- /dev/null +++ b/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/BytesReadTrackerTest.java @@ -0,0 +1,99 @@ +/* + * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + * + * Licensed 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. + */ + +package com.palantir.conjure.java.undertow.runtime; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.conjure.java.undertow.lib.RequestContext; +import io.undertow.Undertow; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public final class BytesReadTrackerTest { + private static final int PORT = 12345; + private static final String HOST = "localhost"; + + private CompletableFuture bytesRead = new CompletableFuture<>(); + private Undertow server; + private ConjureContexts contexts; + + @BeforeEach + public void before() { + bytesRead = new CompletableFuture<>(); + contexts = new ConjureContexts(DefaultRequestArgHandler.INSTANCE); + server = Undertow.builder() + .addHttpListener(PORT, HOST) + .setHandler(exchange -> { + RequestContext context = contexts.createContext(exchange, null); + exchange.getRequestReceiver().receiveFullBytes((_e, _b) -> { + bytesRead.complete(context.bytesRead()); + }); + }) + .build(); + server.start(); + } + + @AfterEach + public void after() { + if (server != null) { + server.stop(); + } + } + + @Test + public void testCountsBytesRead_none() throws Exception { + sendRequest(0); + assertThat(bytesRead.join()).isZero(); + } + + @Test + public void testCountsBytesRead_some() throws Exception { + int some = 1000; + sendRequest(some); + assertThat(bytesRead.join()).isEqualTo(some); + } + + @Test + public void testCountsBytesRead_lots() throws Exception { + int lots = 1024 * 1024; + sendRequest(lots); + assertThat(bytesRead.join()).isEqualTo(lots); + } + + private void sendRequest(int bodySize) throws IOException { + HttpURLConnection connection = (HttpURLConnection) + URI.create("http://" + HOST + ":" + PORT).toURL().openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + + if (bodySize > 0) { + byte[] data = new byte[bodySize]; + try (OutputStream os = connection.getOutputStream()) { + os.write(data); + } + } + + connection.getResponseCode(); // trigger request + connection.disconnect(); + } +} diff --git a/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/RequestContext.java b/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/RequestContext.java index dce10b758..82f6590c4 100644 --- a/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/RequestContext.java +++ b/conjure-undertow-lib/src/main/java/com/palantir/conjure/java/undertow/lib/RequestContext.java @@ -50,6 +50,12 @@ public interface RequestContext { */ Optional firstHeader(String headerName); + /** + * Returns the number of bytes read from the request. + * Returns 0 if byte tracking has not been initialized. + */ + long bytesRead(); + /** * Returns the value of the cookie named {@code cookieName}. * An {@link Optional#empty()} is returned if no such cookie exists. From 152f7668dc9d833338b5fea25d3463beccd206a5 Mon Sep 17 00:00:00 2001 From: daan schouten Date: Sat, 17 Jan 2026 16:32:58 +0100 Subject: [PATCH 2/2] Acknlowledge new interface method --- .palantir/revapi.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .palantir/revapi.yml diff --git a/.palantir/revapi.yml b/.palantir/revapi.yml new file mode 100644 index 000000000..e632b9c74 --- /dev/null +++ b/.palantir/revapi.yml @@ -0,0 +1,7 @@ +acceptedBreaks: + "8.69.0": + com.palantir.conjure.java:conjure-undertow-lib: + - code: "java.method.addedToInterface" + new: "method long com.palantir.conjure.java.undertow.lib.RequestContext::bytesRead()" + justification: "Add bytesRead() method to RequestContext. RequestContext is\ + \ only implemented by conjure-java-undertow-runtime per its documented contract."