Skip to content

Commit

Permalink
Support for send(byte[],int,int) in HTTP/2 responses.
Browse files Browse the repository at this point in the history
Signed-off-by: Santiago Pericas-Geertsen <[email protected]>
  • Loading branch information
spericas committed Dec 6, 2024
1 parent cbe7414 commit 350d3ef
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,17 @@ public Http2ServerResponse header(Header header) {

@Override
public void send(byte[] entityBytes) {
send(entityBytes, 0, entityBytes.length);
}


@Override
public void send(byte[] entityBytes, int position, int length) {
try {
if (outputStreamFilter != null) {
// in this case we must honor user's request to filter the stream
try (OutputStream os = outputStream()) {
os.write(entityBytes);
os.write(entityBytes, position, length);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Expand All @@ -98,25 +104,35 @@ public void send(byte[] entityBytes) {
isSent = true;

// handle content encoding
byte[] bytes = entityBytes(entityBytes);
int actualLength = length;
int actualPosition = position;
byte[] actualBytes = entityBytes(entityBytes, position, length);
if (entityBytes != actualBytes) { // encoding happened, new byte array
actualPosition = 0;
actualLength = actualBytes.length;
}

headers.setIfAbsent(HeaderValues.create(HeaderNames.CONTENT_LENGTH,
true,
false,
String.valueOf(bytes.length)));
headers.setIfAbsent(HeaderValues.create(HeaderNames.DATE, true, false, DateTime.rfc1123String()));

String.valueOf(actualLength)));
headers.setIfAbsent(HeaderValues.create(HeaderNames.DATE, true,
false,
DateTime.rfc1123String()));
Http2Headers http2Headers = Http2Headers.create(headers);
http2Headers.status(status());
headers.remove(Http2Headers.STATUS_NAME, it -> ctx.log(LOGGER,
System.Logger.Level.WARNING,
"Status must be configured on response, "
+ "do not set HTTP/2 pseudo headers"));

boolean sendTrailers = request.headers().contains(HeaderValues.TE_TRAILERS) || headers.contains(HeaderNames.TRAILER);
boolean sendTrailers = request.headers().contains(HeaderValues.TE_TRAILERS)
|| headers.contains(HeaderNames.TRAILER);

http2Headers.validateResponse();
bytesWritten += stream.writeHeadersWithData(http2Headers, bytes.length, BufferData.create(bytes), !sendTrailers);
bytesWritten += stream.writeHeadersWithData(http2Headers, actualLength,
BufferData.create(actualBytes, actualPosition, actualLength),
!sendTrailers);

if (sendTrailers) {
bytesWritten += stream.writeTrailers(Http2Headers.create(trailers));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2024 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.webserver.tests.http2;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

import io.helidon.http.Status;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.testing.junit5.ServerTest;
import io.helidon.webserver.testing.junit5.SetUpRoute;

import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

@ServerTest
class SendBytesTest {
private static final int START = 16;
private static final int LENGTH = 9;
private static final String ENTITY = "The quick brown fox jumps over the lazy dog";

private final HttpClient client;
private final URI uri;

SendBytesTest(URI uri) {
this.uri = uri;
client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.build();
}

@SetUpRoute
static void routing(HttpRules rules) {
rules.get("/sendAll", (req, res) ->
res.send(ENTITY.getBytes(StandardCharsets.UTF_8)))
.get("/sendPart", (req, res) ->
res.send(ENTITY.getBytes(StandardCharsets.UTF_8), START, LENGTH));
}

/**
* Test getting all the entity.
*/
@Test
void testAll() throws IOException, InterruptedException {
HttpResponse<String> response = client.send(HttpRequest.newBuilder()
.uri(uri.resolve("/sendAll"))
.GET()
.build(), HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode(), is(Status.OK_200.code()));
assertThat(response.version(), is(HttpClient.Version.HTTP_2));
String entity = response.body();
assertThat(entity, is(ENTITY));
}

/**
* Test getting part of the entity.
*/
@Test
void testPart() throws IOException, InterruptedException {
HttpResponse<String> response = client.send(HttpRequest.newBuilder()
.uri(uri.resolve("/sendPart"))
.GET()
.build(), HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode(), is(Status.OK_200.code()));
assertThat(response.version(), is(HttpClient.Version.HTTP_2));
String entity = response.body();
assertThat(entity, is(ENTITY.substring(START, START + LENGTH)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2024 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.webserver.tests;

import java.nio.charset.StandardCharsets;

import io.helidon.http.Status;
import io.helidon.webclient.api.HttpClientResponse;
import io.helidon.webclient.http1.Http1Client;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.testing.junit5.ServerTest;
import io.helidon.webserver.testing.junit5.SetUpRoute;

import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

/**
* Tests sending a part of a byte array.
*/
@ServerTest
class SendBytesTest {
private static final int START = 16;
private static final int LENGTH = 9;
private static final String ENTITY = "The quick brown fox jumps over the lazy dog";

private final Http1Client http1Client;

SendBytesTest(Http1Client http1Client) {
this.http1Client = http1Client;
}

@SetUpRoute
static void routing(HttpRules rules) {
rules.get("/sendAll", (req, res) ->
res.send(ENTITY.getBytes(StandardCharsets.UTF_8)))
.get("/sendPart", (req, res) ->
res.send(ENTITY.getBytes(StandardCharsets.UTF_8), START, LENGTH));
}

/**
* Test getting all the entity.
*/
@Test
void testAll() {
try (HttpClientResponse r = http1Client.get("/sendAll").request()) {
String s = r.entity().as(String.class);
assertThat(r.status(), is(Status.OK_200));
assertThat(s, is(ENTITY));
}
}

/**
* Test getting part of the entity.
*/
@Test
void testPart() {
try (HttpClientResponse r = http1Client.get("/sendPart").request()) {
String s = r.entity().as(String.class);
assertThat(r.status(), is(Status.OK_200));
assertThat(s, is(ENTITY.substring(START, START + LENGTH)));
}
}
}

0 comments on commit 350d3ef

Please sign in to comment.