Skip to content

Commit

Permalink
Add HTTP file serving examples
Browse files Browse the repository at this point in the history
Motivation:
There are no examples demonstrating serving response
payload body from file.
  • Loading branch information
Scottmitch committed Jul 16, 2024
1 parent 0a7f539 commit 659eaf0
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
** xref:{page-version}@servicetalk-examples::http/index.adoc#Redirects[Redirects]
** xref:{page-version}@servicetalk-examples::http/index.adoc#Retries[Retries]
** xref:{page-version}@servicetalk-examples::http/index.adoc#uds[Unix Domain Sockets]
** xref:{page-version}@servicetalk-examples::http/index.adoc#Files[Files]
** xref:{page-version}@servicetalk-examples::http/service-composition.adoc[Service Composition]
* xref:{page-version}@servicetalk-examples::grpc/index.adoc[gRPC]
** xref:{page-version}@servicetalk-examples::grpc/index.adoc#HelloWorld[Hello World]
Expand Down
8 changes: 8 additions & 0 deletions servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,11 @@ the link:{source-root}/servicetalk-examples/http/uds[uds example code] for more

NOTE: This example uses the link:#blocking-aggregated[blocking + aggregated] API, as the UDS configuration API is the
same across all the HTTP APIs.

[#Files]
== Files

This example demonstrates asynchronous request processing where the response payload body is streamed from a file.

* link:{source-root}/servicetalk-examples/http/files/src/main/java/io/servicetalk/examples/http/files/FilesServer.java[FilesServer] - a server whose response body is streamed from a file.
* link:{source-root}/servicetalk-examples/http/files/src/main/java/io/servicetalk/examples/http/files/FilesClient.java[FilesClient] - a client that requests and prints response contents.
25 changes: 25 additions & 0 deletions servicetalk-examples/http/files/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors
*
* 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.
*/

apply plugin: "java"
apply from: "../../gradle/idea.gradle"

dependencies {
implementation project(":servicetalk-http-netty")
implementation project(":servicetalk-http-utils")

runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors
*
* 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.servicetalk.examples.http.files;

import io.servicetalk.http.api.HttpClient;
import io.servicetalk.http.netty.HttpClients;
import io.servicetalk.http.utils.TimeoutHttpRequesterFilter;

import java.time.Duration;

import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8;

/**
* Extends the async 'Hello World!' example to demonstrate use of timeout filters and timeout operators. If a single
* timeout can be applied to all transactions then the timeout should be applied using the
* {@link TimeoutHttpRequesterFilter}. If only some transactions require a timeout then the timeout should be applied
* using a {@link io.servicetalk.concurrent.api.Single#timeout(Duration)} Single.timeout()} or a
* {@link io.servicetalk.concurrent.api.Publisher#timeoutTerminal(Duration)} (Duration)} Publisher.timeoutTerminal()}
* operator.
*/
public final class FilesClient {
public static void main(String[] args) throws Exception {
try (HttpClient client = HttpClients.forSingleAddress("localhost", 8080)
.build()) {
// first request, with default timeout from HttpClient (this will succeed)
client.request(client.get("/"))
.whenOnError(System.err::println)
.whenOnSuccess(resp -> {
System.out.println(resp.toString((name, value) -> value));
System.out.println(resp.payloadBody(textSerializerUtf8()));
})
// This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting
// before the response has been processed. This isn't typical usage for an asynchronous API but is useful
// for demonstration purposes.
.toFuture().get();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors
*
* 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.servicetalk.examples.http.files;

import io.servicetalk.buffer.api.BufferAllocator;
import io.servicetalk.http.api.StreamingHttpResponse;
import io.servicetalk.http.netty.HttpServers;
import io.servicetalk.transport.api.IoExecutor;

import java.io.InputStream;

import static io.servicetalk.concurrent.api.Publisher.from;
import static io.servicetalk.concurrent.api.Publisher.fromInputStream;
import static io.servicetalk.concurrent.api.Single.succeeded;
import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE;
import static io.servicetalk.http.api.HttpHeaderValues.TEXT_PLAIN_UTF_8;

/**
* A simple server that serves content from a file via {@link InputStream}. Note reading from file into application
* memory maybe blocking and this example uses the default execution strategy which consumes from the
* {@link InputStream} on a non-{@link IoExecutor} thread to avoid blocking EventLoop threads.
*/
public final class FilesServer {
public static void main(String[] args) throws Exception {
HttpServers.forPort(8080)
.listenStreamingAndAwait((ctx, request, responseFactory) -> {
// InputStream lifetime ownership is transferred to ServiceTalk (e.g. it will call close) because
// we create a new InputStream per request and always pass it to ServiceTalk as the response payload
// body (if not null).
final InputStream responseStream = FilesServer.class.getClassLoader()
.getResourceAsStream("response_payload.txt");
final BufferAllocator allocator = ctx.executionContext().bufferAllocator();
final StreamingHttpResponse response = responseStream == null ?
responseFactory.notFound().payloadBody(
from(allocator.fromAscii("file not found, please rebuild the project"))) :
responseFactory.ok().payloadBody(fromInputStream(responseStream, allocator::wrap));
response.headers().set(CONTENT_TYPE, TEXT_PLAIN_UTF_8);
return succeeded(response);
}).awaitShutdown();
}
}
35 changes: 35 additions & 0 deletions servicetalk-examples/http/files/src/main/resources/log4j2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright © 2024 Apple Inc. and the ServiceTalk project authors
~
~ 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.
-->
<Configuration status="info">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d %30t [%-5level] %-30logger{1} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<!-- Prints server start and shutdown -->
<Logger name="io.servicetalk.http.netty.NettyHttpServer" level="DEBUG"/>

<!-- Prints default subscriber errors-->
<Logger name="io.servicetalk.concurrent.api" level="DEBUG"/>

<!-- Use `-Dservicetalk.logger.level=DEBUG` to change the root logger level via command line -->
<Root level="${sys:servicetalk.logger.level:-INFO}">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
response payload from file!

warning: serving arbitrary files from your server maybe a security risk. You
should take care if user input is used to infer file paths or names and ensure
appropriate access control and auditing is in place.
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,29 @@ public Single<Buffer> multipartHello(@Context final ConnectionContext ctx,
(collector, item) -> ((CompositeBuffer) collector).addBuffer(item));
}

/**
* Resource that streams response content from a file via {@link Publisher}.
* <p>
* Test with:
* <pre>{@code
* curl -v http://localhost:8080/greetings/file-hello
* }</pre>
*
* @param ctx the {@link ConnectionContext}.
* @return greetings as a {@link Single} {@link Buffer}.
*/
@GET
@Path("file-hello")
@Produces(TEXT_PLAIN)
public Publisher<Buffer> multipartHello(@Context final ConnectionContext ctx) {
final InputStream responseStream = HelloWorldJaxRsResource.class.getClassLoader()
.getResourceAsStream("response_payload.txt");
final BufferAllocator allocator = ctx.executionContext().bufferAllocator();
return responseStream == null ?
from(allocator.fromAscii("file not found")) :
fromInputStream(responseStream).map(allocator::wrap);
}

/**
* Resource that only relies on {@link Single}/{@link Publisher} for consuming and producing data,
* and returns a JAX-RS {@link Response} in order to set its status.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
response payload from file!

warning: serving arbitrary files from your server maybe a security risk. You
should take care if user input is used to infer file paths or names and ensure
appropriate access control and auditing is in place.
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ include "servicetalk-annotations",
"servicetalk-examples:http:debugging",
"servicetalk-examples:http:timeout",
"servicetalk-examples:http:defaultloadbalancer",
"servicetalk-examples:http:files",
"servicetalk-gradle-plugin-internal",
"servicetalk-grpc-api",
"servicetalk-grpc-health",
Expand Down Expand Up @@ -160,3 +161,4 @@ project(":servicetalk-examples:http:uds").name = "servicetalk-examples-http-uds"
project(":servicetalk-examples:http:mutual-tls").name = "servicetalk-examples-http-mutual-tls"
project(":servicetalk-examples:http:redirects").name = "servicetalk-examples-http-redirects"
project(":servicetalk-examples:http:compression").name = "servicetalk-examples-http-compression"
project(":servicetalk-examples:http:files").name = "servicetalk-examples-http-files"

0 comments on commit 659eaf0

Please sign in to comment.