diff --git a/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc b/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc index 40943c2a24..4d6a809e69 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc @@ -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] diff --git a/servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc b/servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc index 7c0e3c1cee..26c3c8d071 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc @@ -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. diff --git a/servicetalk-examples/http/files/build.gradle b/servicetalk-examples/http/files/build.gradle new file mode 100644 index 0000000000..acd9cefed0 --- /dev/null +++ b/servicetalk-examples/http/files/build.gradle @@ -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" +} diff --git a/servicetalk-examples/http/files/src/main/java/io/servicetalk/examples/http/files/FilesClient.java b/servicetalk-examples/http/files/src/main/java/io/servicetalk/examples/http/files/FilesClient.java new file mode 100644 index 0000000000..9a1c960298 --- /dev/null +++ b/servicetalk-examples/http/files/src/main/java/io/servicetalk/examples/http/files/FilesClient.java @@ -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(); + } + } +} diff --git a/servicetalk-examples/http/files/src/main/java/io/servicetalk/examples/http/files/FilesServer.java b/servicetalk-examples/http/files/src/main/java/io/servicetalk/examples/http/files/FilesServer.java new file mode 100644 index 0000000000..27ba70eed1 --- /dev/null +++ b/servicetalk-examples/http/files/src/main/java/io/servicetalk/examples/http/files/FilesServer.java @@ -0,0 +1,56 @@ +/* + * 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). + // Note that File APIs are blocking. ServiceTalk by default will call the File APIs on a non-IoExecutor thread + // and it isn't recommended to disable offloading for code paths that interact with blocking File APIs. + 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.fromUtf8("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(); + } +} diff --git a/servicetalk-examples/http/files/src/main/resources/log4j2.xml b/servicetalk-examples/http/files/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..feaac43cdb --- /dev/null +++ b/servicetalk-examples/http/files/src/main/resources/log4j2.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/servicetalk-examples/http/files/src/main/resources/response_payload.txt b/servicetalk-examples/http/files/src/main/resources/response_payload.txt new file mode 100644 index 0000000000..a8686cbfc5 --- /dev/null +++ b/servicetalk-examples/http/files/src/main/resources/response_payload.txt @@ -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. \ No newline at end of file diff --git a/servicetalk-examples/http/jaxrs/src/main/java/io/servicetalk/examples/http/jaxrs/HelloWorldJaxRsResource.java b/servicetalk-examples/http/jaxrs/src/main/java/io/servicetalk/examples/http/jaxrs/HelloWorldJaxRsResource.java index b66895bd5d..24daf2b3dc 100644 --- a/servicetalk-examples/http/jaxrs/src/main/java/io/servicetalk/examples/http/jaxrs/HelloWorldJaxRsResource.java +++ b/servicetalk-examples/http/jaxrs/src/main/java/io/servicetalk/examples/http/jaxrs/HelloWorldJaxRsResource.java @@ -224,6 +224,34 @@ public Single multipartHello(@Context final ConnectionContext ctx, (collector, item) -> ((CompositeBuffer) collector).addBuffer(item)); } + /** + * Resource that streams response content from a file via {@link Publisher}. + *

+ * Test with: + *

{@code
+     * curl -v http://localhost:8080/greetings/file-hello
+     * }
+ * + * @param ctx the {@link ConnectionContext}. + * @return greetings as a {@link Single} {@link Buffer}. + */ + @GET + @Path("file-hello") + @Produces(TEXT_PLAIN) + public Publisher multipartHello(@Context final ConnectionContext ctx) { + final InputStream responseStream = HelloWorldJaxRsResource.class.getClassLoader() + .getResourceAsStream("response_payload.txt"); + final BufferAllocator allocator = ctx.executionContext().bufferAllocator(); + // 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). + // Note that File APIs are blocking. ServiceTalk by default will call the File APIs on a non-IoExecutor thread + // and it isn't recommended to disable offloading for code paths that interact with blocking File APIs. + 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. diff --git a/servicetalk-examples/http/jaxrs/src/main/resources/response_payload.txt b/servicetalk-examples/http/jaxrs/src/main/resources/response_payload.txt new file mode 100644 index 0000000000..a8686cbfc5 --- /dev/null +++ b/servicetalk-examples/http/jaxrs/src/main/resources/response_payload.txt @@ -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. \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index f99fcd6b63..646bd8e241 100755 --- a/settings.gradle +++ b/settings.gradle @@ -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", @@ -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"