Skip to content
Open
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
7 changes: 7 additions & 0 deletions .palantir/revapi.yml
Original file line number Diff line number Diff line change
@@ -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."
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -27,5 +28,7 @@ public final class Attachments {

public static final AttachmentKey<Throwable> FAILURE = AttachmentKey.create(Throwable.class);

static final AttachmentKey<LongSupplier> BYTES_READ = AttachmentKey.create(LongSupplier.class);

private Attachments() {}
}
Original file line number Diff line number Diff line change
@@ -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<StreamSourceConduit> 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() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Long> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ public interface RequestContext {
*/
Optional<String> 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.
Expand Down