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

Add HTTP file serving examples #3010

Merged
merged 3 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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"))) :
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
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, 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"
Loading