Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.x: HTTP/2.0 Client trailers support #6544 #7516

Merged
merged 1 commit into from
Sep 18, 2023
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,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