Skip to content

Commit

Permalink
HTTP/2.0 Client trailers support helidon-io#6544 (helidon-io#7516)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielkec authored Sep 18, 2023
1 parent c976f85 commit ecdef53
Show file tree
Hide file tree
Showing 19 changed files with 573 additions and 115 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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;

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

/**
* Create new trailers from headers future.
*
* @param headers trailer headers
* @return new client trailers from headers future
*/
static ClientResponseTrailers create(io.helidon.http.Headers headers) {
return new ClientResponseTrailersImpl(headers);
}

/**
* Create new empty trailers.
*
* @return new empty client trailers
*/
static ClientResponseTrailers create() {
return new ClientResponseTrailersImpl();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.util.Iterator;
import java.util.List;
import java.util.function.Supplier;

class ClientResponseTrailersImpl implements ClientResponseTrailers {
private static final Headers EMPTY_TRAILERS = WritableHeaders.create();
private final Headers trailers;

ClientResponseTrailersImpl(Headers trailers) {
this.trailers = trailers;
}

ClientResponseTrailersImpl() {
this.trailers = EMPTY_TRAILERS;
}

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

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

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

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

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

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

@Override
public Iterator<Header> iterator() {
return trailers.iterator();
}
}
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.Status;

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

/**
* Response trailer headers.
* Blocks until trailers are available.
*
* @throws java.lang.IllegalStateException when invoked before entity is requested
* @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.Status;

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.Status;

/**
* 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
*/
CompletableFuture<ClientResponseTrailers> trailers();

/**
* Status of the response.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 java.util.concurrent.CompletableFuture;

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(CompletableFuture.completedFuture(ClientResponseTrailers.create()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,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 @@ -17,9 +17,13 @@
package io.helidon.webclient.http1;

import java.io.InputStream;
import java.time.Duration;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

import io.helidon.common.GenericType;
Expand All @@ -28,11 +32,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.HeaderNames;
import io.helidon.http.HeaderValues;
import io.helidon.http.Http1HeadersParser;
import io.helidon.http.Status;
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 @@ -49,9 +53,10 @@ class Http1ClientResponseImpl implements Http1ClientResponse {
@SuppressWarnings("rawtypes")
private static final List<SourceHandlerProvider> SOURCE_HANDLERS
= HelidonServiceLoader.builder(ServiceLoader.load(SourceHandlerProvider.class)).build().asList();

private static final long ENTITY_LENGTH_CHUNKED = -1;
private final AtomicBoolean closed = new AtomicBoolean();

private final HttpClientConfig clientConfig;
private final Status responseStatus;
private final ClientRequestHeaders requestHeaders;
private final ClientResponseHeaders responseHeaders;
Expand All @@ -65,9 +70,9 @@ class Http1ClientResponseImpl implements Http1ClientResponse {
private final ClientUri lastEndpointUri;

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

Http1ClientResponseImpl(HttpClientConfig clientConfig,
Status responseStatus,
Expand All @@ -79,6 +84,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 All @@ -92,7 +98,7 @@ class Http1ClientResponseImpl implements Http1ClientResponse {
if (responseHeaders.contains(HeaderNames.CONTENT_LENGTH)) {
this.entityLength = Long.parseLong(responseHeaders.get(HeaderNames.CONTENT_LENGTH).value());
} else if (responseHeaders.contains(HeaderValues.TRANSFER_ENCODING_CHUNKED)) {
this.entityLength = -1;
this.entityLength = ENTITY_LENGTH_CHUNKED;
}
if (responseHeaders.contains(HeaderNames.TRAILER)) {
this.hasTrailers = true;
Expand All @@ -113,8 +119,38 @@ public ClientResponseHeaders headers() {
return responseHeaders;
}

@Override
public ClientResponseTrailers trailers() {
if (hasTrailers) {
// Block until trailers arrive
Duration timeout = clientConfig.readTimeout()
.orElseGet(() -> clientConfig.socketOptions().readTimeout());

if (!this.entityRequested) {
throw new IllegalStateException("Trailers requested before reading entity.");
}

try {
return ClientResponseTrailers.create(this.trailers.get(timeout.toMillis(), TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
throw new IllegalStateException("Timeout " + timeout + " reached while waiting for trailers.", e);
} catch (InterruptedException e) {
throw new IllegalStateException("Interrupted while waiting for trailers.", e);
} catch (ExecutionException e) {
if (e.getCause() instanceof IllegalStateException ise) {
throw ise;
} else {
throw new IllegalStateException(e.getCause());
}
}
} else {
return ClientResponseTrailers.create();
}
}

@Override
public ReadableEntity entity() {
this.entityRequested = true;
return entity(requestHeaders, responseHeaders);
}

Expand All @@ -125,11 +161,15 @@ public void close() {
if (headers().contains(HeaderValues.CONNECTION_CLOSE)) {
connection.closeResource();
} else {
if (entityFullyRead || entityLength == 0) {
if (entityLength == 0) {
connection.releaseResource();
} else if (entityLength == ENTITY_LENGTH_CHUNKED) {
if (hasTrailers) {
readTrailers();
connection.releaseResource();
} else {
connection.closeResource();
}
connection.releaseResource();
} else {
connection.closeResource();
}
Expand Down Expand Up @@ -176,7 +216,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
Loading

0 comments on commit ecdef53

Please sign in to comment.