/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import com.linecorp.armeria.common.Request; import com.linecorp.armeria.common.http.AggregatedHttpMessage; import com.linecorp.armeria.common.http.HttpHeaders; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.common.http.HttpResponseWriter; import com.linecorp.armeria.common.http.HttpStatus; import com.linecorp.armeria.common.util.CompletionActions; import com.linecorp.armeria.common.util.Exceptions; import com.linecorp.armeria.server.http.AbstractHttpService; import com.linecorp.armeria.server.logging.LoggingService; import com.linecorp.armeria.testing.server.ServerRule; import io.netty.handler.codec.http.HttpStatusClass; import io.netty.util.concurrent.DefaultEventExecutorGroup; import io.netty.util.concurrent.EventExecutorGroup; public class ServerTest { private static final long processDelayMillis = 1000; private static final long requestTimeoutMillis = 500; private static final long idleTimeoutMillis = 500; private static final EventExecutorGroup asyncExecutorGroup = new DefaultEventExecutorGroup(1); @ClassRule public static final ServerRule server = new ServerRule() { @Override protected void configure(ServerBuilder sb) throws Exception { final Service<HttpRequest, HttpResponse> immediateResponseOnIoThread = new EchoService().decorate(LoggingService::new); final Service<HttpRequest, HttpResponse> delayedResponseOnIoThread = new EchoService() { @Override protected void echo(AggregatedHttpMessage aReq, HttpResponseWriter res) { try { Thread.sleep(processDelayMillis); super.echo(aReq, res); } catch (InterruptedException e) { e.printStackTrace(); } } }.decorate(LoggingService::new); final Service<HttpRequest, HttpResponse> lazyResponseNotOnIoThread = new EchoService() { @Override protected void echo(AggregatedHttpMessage aReq, HttpResponseWriter res) { asyncExecutorGroup.schedule( () -> super.echo(aReq, res), processDelayMillis, TimeUnit.MILLISECONDS); } }.decorate(LoggingService::new); final Service<HttpRequest, HttpResponse> buggy = new AbstractHttpService() { @Override protected void doPost(ServiceRequestContext ctx, HttpRequest req, HttpResponseWriter res) throws Exception { throw Exceptions.clearTrace(new Exception("bug!")); } }.decorate(LoggingService::new); sb.serviceAt("/", immediateResponseOnIoThread) .serviceAt("/delayed", delayedResponseOnIoThread) .serviceAt("/timeout", lazyResponseNotOnIoThread) .serviceAt("/timeout-not", lazyResponseNotOnIoThread) .serviceAt("/buggy", buggy); // Disable request timeout for '/timeout-not' only. final Function<Service<HttpRequest, HttpResponse>, Service<HttpRequest, HttpResponse>> decorator = s -> new SimpleDecoratingService<HttpRequest, HttpResponse>(s) { @Override public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { ctx.setRequestTimeoutMillis( "/timeout-not".equals(ctx.path()) ? 0 : requestTimeoutMillis); return delegate().serve(ctx, req); } }; sb.decorator(decorator); sb.idleTimeoutMillis(idleTimeoutMillis); } }; /** * Ensures that the {@link Server} is always started when a test begins. This is necessary even if we * enabled auto-start for {@link ServerRule} because we stop it in {@link #testStartStop()}. */ @Before public void startServer() { server.start(); } @Test public void testStartStop() throws Exception { final Server server = ServerTest.server.server(); assertThat(server.activePorts().size(), is(1)); server.stop().get(); assertThat(server.activePorts().size(), is(0)); } @Test public void testInvocation() throws Exception { testInvocation0("/"); } @Test public void testDelayedResponseApiInvocationExpectedTimeout() throws Exception { testInvocation0("/delayed"); } private static void testInvocation0(String path) throws IOException { try (CloseableHttpClient hc = HttpClients.createMinimal()) { final HttpPost req = new HttpPost(server.uri(path)); req.setEntity(new StringEntity("Hello, world!", StandardCharsets.UTF_8)); try (CloseableHttpResponse res = hc.execute(req)) { assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK")); assertThat(EntityUtils.toString(res.getEntity()), is("Hello, world!")); } } } @Test public void testRequestTimeoutInvocation() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal()) { final HttpPost req = new HttpPost(server.uri("/timeout")); req.setEntity(new StringEntity("Hello, world!", StandardCharsets.UTF_8)); try (CloseableHttpResponse res = hc.execute(req)) { assertThat(HttpStatusClass.valueOf(res.getStatusLine().getStatusCode()), is(not(HttpStatusClass.SUCCESS))); } } } @Test public void testDynamicRequestTimeoutInvocation() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal()) { final HttpPost req = new HttpPost(server.uri("/timeout-not")); req.setEntity(new StringEntity("Hello, world!", StandardCharsets.UTF_8)); try (CloseableHttpResponse res = hc.execute(req)) { assertThat(HttpStatusClass.valueOf(res.getStatusLine().getStatusCode()), is(HttpStatusClass.SUCCESS)); } } } @Test(timeout = idleTimeoutMillis * 5) public void testIdleTimeoutByNoContentSent() throws Exception { try (Socket socket = new Socket()) { socket.setSoTimeout((int) (idleTimeoutMillis * 4)); socket.connect(server.httpSocketAddress()); long connectedNanos = System.nanoTime(); //read until EOF while (socket.getInputStream().read() != -1) { continue; } long elapsedTimeMillis = TimeUnit.MILLISECONDS.convert( System.nanoTime() - connectedNanos, TimeUnit.NANOSECONDS); assertThat(elapsedTimeMillis, is(greaterThanOrEqualTo(idleTimeoutMillis))); } } @Test(timeout = idleTimeoutMillis * 5) public void testIdleTimeoutByContentSent() throws Exception { try (Socket socket = new Socket()) { socket.setSoTimeout((int) (idleTimeoutMillis * 4)); socket.connect(server.httpSocketAddress()); PrintWriter outWriter = new PrintWriter(socket.getOutputStream(), false); outWriter.print("POST / HTTP/1.1\r\n"); outWriter.print("Connection: Keep-Alive\r\n"); outWriter.print("\r\n"); outWriter.flush(); long lastWriteNanos = System.nanoTime(); //read until EOF while (socket.getInputStream().read() != -1) { continue; } long elapsedTimeMillis = TimeUnit.MILLISECONDS.convert( System.nanoTime() - lastWriteNanos, TimeUnit.NANOSECONDS); assertThat(elapsedTimeMillis, is(greaterThanOrEqualTo(idleTimeoutMillis))); } } /** * Ensure that the connection is not broken even if {@link Service#serve(ServiceRequestContext, Request)} * raises an exception. */ @Test(timeout = idleTimeoutMillis * 5) public void testBuggyService() throws Exception { try (Socket socket = new Socket()) { socket.setSoTimeout((int) (idleTimeoutMillis * 4)); socket.connect(server.httpSocketAddress()); PrintWriter outWriter = new PrintWriter(socket.getOutputStream(), false); // Send a request to a buggy service whose invoke() raises an exception. // If the server handled the exception correctly (i.e. responded with 500 Internal Server Error and // recovered from the exception successfully), then the connection should not be closed immediately // but on the idle timeout of the second request. outWriter.print("POST /buggy HTTP/1.1\r\n"); outWriter.print("Connection: Keep-Alive\r\n"); outWriter.print("Content-Length: 0\r\n"); outWriter.print("\r\n"); outWriter.print("POST / HTTP/1.1\r\n"); outWriter.print("Connection: Keep-Alive\r\n"); outWriter.print("\r\n"); outWriter.flush(); long lastWriteNanos = System.nanoTime(); //read until EOF while (socket.getInputStream().read() != -1) { continue; } long elapsedTimeMillis = TimeUnit.MILLISECONDS.convert( System.nanoTime() - lastWriteNanos, TimeUnit.NANOSECONDS); assertThat(elapsedTimeMillis, is(greaterThanOrEqualTo(idleTimeoutMillis))); } } @Test public void testOptions() throws Exception { testSimple("OPTIONS * HTTP/1.1", "HTTP/1.1 200 OK", "allow: OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE,TRACE"); } @Test public void testInvalidPath() throws Exception { testSimple("GET * HTTP/1.1", "HTTP/1.1 400 Bad Request"); } private static void testSimple( String reqLine, String expectedStatusLine, String... expectedHeaders) throws Exception { try (Socket socket = new Socket()) { socket.setSoTimeout((int) (idleTimeoutMillis * 4)); socket.connect(server.httpSocketAddress()); PrintWriter outWriter = new PrintWriter(socket.getOutputStream(), false); outWriter.print(reqLine); outWriter.print("\r\n"); outWriter.print("Connection: close\r\n"); outWriter.print("Content-Length: 0\r\n"); outWriter.print("\r\n"); outWriter.flush(); BufferedReader in = new BufferedReader(new InputStreamReader( socket.getInputStream(), StandardCharsets.US_ASCII)); assertThat(in.readLine(), is(expectedStatusLine)); // Read till the end of the connection. List<String> headers = new ArrayList<>(); for (;;) { String line = in.readLine(); if (line == null) { break; } // This is not really correct, but just wanna make it as simple as possible. headers.add(line); } for (String expectedHeader : expectedHeaders) { if (!headers.contains(expectedHeader)) { fail("does not contain '" + expectedHeader + "': " + headers); } } } } private static class EchoService extends AbstractHttpService { @Override protected final void doPost(ServiceRequestContext ctx, HttpRequest req, HttpResponseWriter res) { req.aggregate() .thenAccept(aReq -> echo(aReq, res)) .exceptionally(CompletionActions::log); } protected void echo(AggregatedHttpMessage aReq, HttpResponseWriter res) { res.write(HttpHeaders.of(HttpStatus.OK)); res.write(aReq.content()); res.close(); } } }