Skip to content

Commit

Permalink
HTTP/2.0 Client trailers support helidon-io#6544
Browse files Browse the repository at this point in the history
  • Loading branch information
danielkec committed Sep 5, 2023
1 parent f3aca7a commit 8dd8dfb
Show file tree
Hide file tree
Showing 18 changed files with 492 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates.
*
* 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 io.helidon.http;

import java.time.Duration;
import java.util.concurrent.CompletableFuture;

/**
* HTTP Trailer headers of a client response.
*/
public interface ClientResponseTrailers extends io.helidon.http.Headers {

/**
* Create new trailers from headers future.
*
* @param trailerFuture trailer headers future completed when trailers are received.
* @param timeout timeout for blocking till trailers are received
* @return new client trailers from headers future
*/
static ClientResponseTrailers create(CompletableFuture<io.helidon.http.Headers> trailerFuture, Duration timeout) {
return new ClientResponseTrailersImpl(trailerFuture, timeout);
}

/**
* Create new empty trailers.
*
* @return new empty client trailers
*/
static ClientResponseTrailers create() {
return new ClientResponseTrailersImpl();
}

/**
* Returns true when trailer headers have been fully received.
*
* @return true if received
*/
boolean received();

/**
* Returns true if trailer headers can be expected.
*
* @return true if expected
*/
boolean expected();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates.
*
* 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 io.helidon.http;

import java.time.Duration;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;

class ClientResponseTrailersImpl implements ClientResponseTrailers {
private static final CompletableFuture<Headers> EMPTY_TRAILERS = CompletableFuture.completedFuture(WritableHeaders.create());
private final CompletableFuture<Headers> trailerFuture;
private final Duration timeout;

ClientResponseTrailersImpl(CompletableFuture<Headers> trailerFuture, Duration timeout) {
this.trailerFuture = trailerFuture;
this.timeout = timeout;
}

ClientResponseTrailersImpl() {
this.trailerFuture = EMPTY_TRAILERS;
this.timeout = Duration.ZERO;
}

@Override
public boolean received() {
return trailerFuture.isDone() && expected();
}

@Override
public boolean expected() {
return trailerFuture != EMPTY_TRAILERS;
}

@Override
public List<String> all(Http.HeaderName name, Supplier<List<String>> defaultSupplier) {
return getWithTimeout().all(name, defaultSupplier);
}

@Override
public boolean contains(Http.HeaderName name) {
return getWithTimeout().contains(name);
}

@Override
public boolean contains(Http.Header value) {
return getWithTimeout().contains(value);
}

@Override
public Http.Header get(Http.HeaderName name) {
return getWithTimeout().get(name);
}

@Override
public int size() {
return getWithTimeout().size();
}

@Override
public List<HttpMediaType> acceptedTypes() {
return getWithTimeout().acceptedTypes();
}

@Override
public Iterator<Http.Header> iterator() {
return getWithTimeout().iterator();
}

private Headers getWithTimeout() {
try {
return trailerFuture.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new IllegalStateException("Timeout " + timeout + " reached while waiting for trailers.", e);
}
}
}
10 changes: 9 additions & 1 deletion http/http2/src/main/java/io/helidon/http/http2/Http2Stream.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,19 @@ public interface Http2Stream {
/**
* Headers received.
*
* @param headers request headers
* @param headers headers
* @param endOfStream whether these headers are the last data that would be received
*/
void headers(Http2Headers headers, boolean endOfStream);

/**
* Trailers received.
*
* @param headers trailer headers
* @param endOfStream whether these headers are the last data that would be received
*/
void trailers(Http2Headers headers, boolean endOfStream);

/**
* Data frame.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.helidon.webclient.api;

import io.helidon.http.ClientResponseHeaders;
import io.helidon.http.ClientResponseTrailers;
import io.helidon.http.Http;

/**
Expand All @@ -37,6 +38,13 @@ interface ClientResponseBase {
*/
ClientResponseHeaders headers();

/**
* Response trailer headers.
*
* @return trailers
*/
ClientResponseTrailers trailers();

/**
* URI of the last request. (after redirection)
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.helidon.webclient.api;

import io.helidon.http.ClientResponseHeaders;
import io.helidon.http.ClientResponseTrailers;
import io.helidon.http.Http;

class ClientResponseTypedImpl<T> implements ClientResponseTyped<T> {
Expand Down Expand Up @@ -51,6 +52,11 @@ public ClientResponseHeaders headers() {
return response.headers();
}

@Override
public ClientResponseTrailers trailers() {
return response.trailers();
}

@Override
public ClientUri lastEndpointUri() {
return response.lastEndpointUri();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@

import io.helidon.builder.api.Prototype;
import io.helidon.http.ClientResponseHeaders;
import io.helidon.http.ClientResponseTrailers;
import io.helidon.http.Http;

/**
* Response which is created upon receiving of server response.
*/
@Prototype.Blueprint
@Prototype.Blueprint(decorator = WebClientServiceResponseDecorator.class)
interface WebClientServiceResponseBlueprint {

/**
Expand All @@ -37,6 +38,13 @@ interface WebClientServiceResponseBlueprint {
*/
ClientResponseHeaders headers();

/**
* Received response trailer headers.
*
* @return immutable response trailer headers
*/
ClientResponseTrailers trailers();

/**
* Status of the response.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates.
*
* 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 io.helidon.webclient.api;

import io.helidon.builder.api.Prototype;
import io.helidon.http.ClientResponseTrailers;

class WebClientServiceResponseDecorator implements Prototype.BuilderDecorator<WebClientServiceResponse.BuilderBase<?, ?>> {
@Override
public void decorate(WebClientServiceResponse.BuilderBase<?, ?> target) {
if (target.trailers().isEmpty()) {
// Empty trailers by default
target.trailers(ClientResponseTrailers.create());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,10 @@ private void ensureBuffer() {
+ BufferData.create(hex.getBytes(US_ASCII)).debugDataHex());
}
if (length == 0) {
reader.skip(2); // second CRLF finishing the entity

if (reader.startsWithNewLine()) {
// No trailers, skip second CRLF
reader.skip(2);
}
helidonSocket.log(LOGGER, TRACE, "read last (empty) chunk");
finished = true;
currentBuffer = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@
import io.helidon.common.media.type.ParserMode;
import io.helidon.http.ClientRequestHeaders;
import io.helidon.http.ClientResponseHeaders;
import io.helidon.http.ClientResponseTrailers;
import io.helidon.http.Http;
import io.helidon.http.Http.HeaderNames;
import io.helidon.http.Http.Headers;
import io.helidon.http.Http1HeadersParser;
import io.helidon.http.WritableHeaders;
import io.helidon.http.media.MediaContext;
import io.helidon.http.media.ReadableEntity;
import io.helidon.http.media.ReadableEntityBase;
Expand All @@ -52,6 +52,7 @@ class Http1ClientResponseImpl implements Http1ClientResponse {

private final AtomicBoolean closed = new AtomicBoolean();

private final HttpClientConfig clientConfig;
private final Http.Status responseStatus;
private final ClientRequestHeaders requestHeaders;
private final ClientResponseHeaders responseHeaders;
Expand All @@ -66,8 +67,8 @@ class Http1ClientResponseImpl implements Http1ClientResponse {

private final ClientConnection connection;
private long entityLength;
private final CompletableFuture<io.helidon.http.Headers> trailers = new CompletableFuture<>();
private boolean entityFullyRead;
private WritableHeaders<?> trailers;

Http1ClientResponseImpl(HttpClientConfig clientConfig,
Http.Status responseStatus,
Expand All @@ -79,6 +80,7 @@ class Http1ClientResponseImpl implements Http1ClientResponse {
ParserMode parserMode,
ClientUri lastEndpointUri,
CompletableFuture<Void> whenComplete) {
this.clientConfig = clientConfig;
this.responseStatus = responseStatus;
this.requestHeaders = requestHeaders;
this.responseHeaders = responseHeaders;
Expand Down Expand Up @@ -113,6 +115,16 @@ public ClientResponseHeaders headers() {
return responseHeaders;
}

@Override
public ClientResponseTrailers trailers() {
if (hasTrailers) {
return ClientResponseTrailers.create(this.trailers, this.clientConfig.readTimeout()
.orElseGet(() -> this.clientConfig.socketOptions().readTimeout()));
} else {
return ClientResponseTrailers.create();
}
}

@Override
public ReadableEntity entity() {
return entity(requestHeaders, responseHeaders);
Expand All @@ -125,7 +137,7 @@ public void close() {
if (headers().contains(Http.Headers.CONNECTION_CLOSE)) {
connection.closeResource();
} else {
if (entityFullyRead || entityLength == 0) {
if (entityLength == -1) {
if (hasTrailers) {
readTrailers();
}
Expand Down Expand Up @@ -176,7 +188,7 @@ private ReadableEntity entity(ClientRequestHeaders requestHeaders,
}

private void readTrailers() {
this.trailers = Http1HeadersParser.readHeaders(connection.reader(), 1024, true);
this.trailers.complete(Http1HeadersParser.readHeaders(connection.reader(), 1024, true));
}

private BufferData readBytes(int estimate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.NoSuchElementException;

import io.helidon.http.ClientResponseHeaders;
import io.helidon.http.ClientResponseTrailers;
import io.helidon.http.Http;
import io.helidon.http.media.ReadableEntity;
import io.helidon.webclient.api.ClientConnection;
Expand Down Expand Up @@ -117,6 +118,11 @@ public ClientResponseHeaders headers() {
return delegate.headers();
}

@Override
public ClientResponseTrailers trailers() {
return delegate.trailers();
}

@Override
public ClientUri lastEndpointUri() {
return delegate.lastEndpointUri();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ protected WebClientServiceResponse readResponse(WebClientServiceRequest serviceR
.whenComplete(whenComplete)
.status(responseStatus)
.headers(responseHeaders)
.trailers(stream.trailers())
.connection(stream)
.build();

Expand Down
Loading

0 comments on commit 8dd8dfb

Please sign in to comment.