diff --git a/core/src/main/java/spotty/common/http/HttpHeaders.java b/core/src/main/java/spotty/common/http/HttpHeaders.java index 9371d97..b42bef6 100644 --- a/core/src/main/java/spotty/common/http/HttpHeaders.java +++ b/core/src/main/java/spotty/common/http/HttpHeaders.java @@ -315,7 +315,7 @@ public HttpHeaders(HttpHeaders headers) { /** * add header * - * @param name header name + * @param name header name * @param value header value * @return this instance of headers */ @@ -360,39 +360,39 @@ public String get(String name) { * remove header by name * * @param name header name - * @return the previous header value associated with name, or - * null if there was no header for given name. + * @return the previous header value associated with name, or + * null if there was no header for given name. */ public String remove(String name) { return headers.remove(name); } /** - * Returns true if this HttpHeaders contains a header for the specified name. + * Returns true if this HttpHeaders contains a header for the specified name. * * @param name header name - * @return true if this HttpHeaders contains a header for the specified name. + * @return true if this HttpHeaders contains a header for the specified name. */ public boolean has(String name) { return headers.containsKey(name); } /** - * Returns false if this HttpHeaders contains no header for the specified name. + * Returns false if this HttpHeaders contains no header for the specified name. * * @param name header name - * @return true if this HttpHeaders contains no header for the specified name. + * @return true if this HttpHeaders contains no header for the specified name. */ public boolean hasNot(String name) { return !headers.containsKey(name); } /** - * Returns true if this HttpHeaders contains a header for the specified name and header value is equal with given. + * Returns true if this HttpHeaders contains a header for the specified name and header value is equal with given. * - * @param name header name + * @param name header name * @param value header value - * @return true if this HttpHeaders contains a header for the specified name and header value is equal with given. + * @return true if this HttpHeaders contains a header for the specified name and header value is equal with given. */ public boolean hasAndEqual(String name, String value) { final String header = headers.get(name); @@ -413,18 +413,18 @@ public int size() { } /** - * Returns true if this HttpHeaders contains no headers. + * Returns true if this HttpHeaders contains no headers. * - * @return true if this HttpHeaders contains no headers + * @return true if this HttpHeaders contains no headers */ public boolean isEmpty() { return headers.isEmpty(); } /** - * Returns true if this HttpHeaders contains headers. + * Returns true if this HttpHeaders contains headers. * - * @return true if this HttpHeaders contains headers + * @return true if this HttpHeaders contains headers */ public boolean isNotEmpty() { return headers.size() > 0; diff --git a/core/src/main/java/spotty/common/response/ResponseHeadersWriter.java b/core/src/main/java/spotty/common/response/ResponseHeadersWriter.java new file mode 100644 index 0000000..66a40f8 --- /dev/null +++ b/core/src/main/java/spotty/common/response/ResponseHeadersWriter.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 - Alex Danilenko + * + * 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 spotty.common.response; + +import spotty.common.http.HttpHeaders; +import spotty.common.stream.output.SpottyByteArrayOutputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class ResponseHeadersWriter { + private static final byte[] HEADER_SPLITTER = ": ".getBytes(UTF_8); + private static final byte[] SPACE = " ".getBytes(UTF_8); + private static final byte[] CONTENT_LENGTH = HttpHeaders.CONTENT_LENGTH.getBytes(UTF_8); + private static final byte[] CONTENT_TYPE = HttpHeaders.CONTENT_TYPE.getBytes(UTF_8); + private static final byte[] SET_COOKIE = HttpHeaders.SET_COOKIE.getBytes(UTF_8); + + public static void write(SpottyByteArrayOutputStream writer, SpottyResponse response) { + writer.print(response.protocol().code); writer.write(SPACE); writer.print(response.status().toString()); + writer.println(); + + writer.write(CONTENT_LENGTH); writer.write(HEADER_SPLITTER); writer.print(Integer.toString(response.contentLength())); + writer.println(); + + if (response.contentType() != null) { + writer.write(CONTENT_TYPE); writer.write(HEADER_SPLITTER); writer.print(response.contentType()); + writer.println(); + } + + response.headers() + .forEach((name, value) -> { + writer.print(name); writer.write(HEADER_SPLITTER); writer.print(value); + writer.println(); + }); + + response.cookies() + .forEach(cookie -> { + writer.write(SET_COOKIE); writer.write(HEADER_SPLITTER); writer.print(cookie.toString()); + writer.println(); + }); + + writer.println(); + } + +} diff --git a/core/src/main/java/spotty/common/response/ResponseWriter.java b/core/src/main/java/spotty/common/response/ResponseWriter.java deleted file mode 100644 index 3c01118..0000000 --- a/core/src/main/java/spotty/common/response/ResponseWriter.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022 - Alex Danilenko - * - * 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 spotty.common.response; - -import spotty.common.http.HttpHeaders; -import spotty.common.stream.output.SpottyByteArrayOutputStream; - -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * Single thread use only - */ -public final class ResponseWriter { - private static final byte[] HEADER_SPLITTER = ": ".getBytes(UTF_8); - private static final byte[] SPACE = " ".getBytes(UTF_8); - private static final byte[] CONTENT_LENGTH = HttpHeaders.CONTENT_LENGTH.getBytes(UTF_8); - private static final byte[] CONTENT_TYPE = HttpHeaders.CONTENT_TYPE.getBytes(UTF_8); - private static final byte[] SET_COOKIE = HttpHeaders.SET_COOKIE.getBytes(UTF_8); - - private final SpottyByteArrayOutputStream writer = new SpottyByteArrayOutputStream(2048); - - public byte[] write(SpottyResponse response) { - try { - writer.print(response.protocol().code); writer.write(SPACE); writer.print(response.status().toString()); - writer.println(); - - writer.write(CONTENT_LENGTH); writer.write(HEADER_SPLITTER); writer.print(Integer.toString(response.contentLength())); - writer.println(); - - if (response.contentType() != null) { - writer.write(CONTENT_TYPE); writer.write(HEADER_SPLITTER); writer.print(response.contentType()); - writer.println(); - } - - response.headers() - .forEach((name, value) -> { - writer.print(name); writer.write(HEADER_SPLITTER); writer.print(value); - writer.println(); - }); - - response.cookies() - .forEach(cookie -> { - writer.write(SET_COOKIE); writer.write(HEADER_SPLITTER); writer.print(cookie.toString()); - writer.println(); - }); - - writer.println(); - - if (response.body() != null) { - writer.write(response.body()); - } - - return writer.toByteArray(); - } finally { - writer.reset(); - } - } - -} diff --git a/core/src/main/java/spotty/common/session/Session.java b/core/src/main/java/spotty/common/session/Session.java index a9692be..195deec 100644 --- a/core/src/main/java/spotty/common/session/Session.java +++ b/core/src/main/java/spotty/common/session/Session.java @@ -112,7 +112,7 @@ public Session putIfAbsent(Object key, Object value) { * * @param key key with which the specified value is to be associated * @param mapper the function to compute a value - * @param return type + * @param return type * @return the current (existing or computed) value associated with * the specified key, or null if the computed value is null */ @@ -132,7 +132,7 @@ public T computeIfAbsent(Object key, Function mapper) { * * @param key key with which the specified value is to be associated * @param mapper the function to compute a value - * @param return type + * @param return type * @return the current (existing or computed) value associated with * the specified key, or null if the computed value is null */ @@ -154,7 +154,7 @@ public T computeIfPresent(Object key, BiFunction mapper) { * * @param key key with which the specified value is to be associated * @param mapper the function to compute a value - * @param return type + * @param return type * @return the new value associated with the specified key, or null if none */ @SuppressWarnings("all") @@ -192,7 +192,7 @@ public T get(Object key) { * * @param key the key whose associated value is to be returned * @param defaultValue the default mapping of the key - * @param return type + * @param return type * @return the value to which the specified key is mapped, or * {@code defaultValue} if this map contains no mapping for the key */ @@ -209,14 +209,14 @@ public int size() { } /** - * @return true if this session contains no key-value mappings + * @return true if this session contains no key-value mappings */ public boolean isEmpty() { return data.isEmpty(); } /** - * @return true if this session contains key-value mappings + * @return true if this session contains key-value mappings */ public boolean isNotEmpty() { return data.size() > 0; @@ -271,31 +271,31 @@ public Set> entrySet() { } /** - * Returns true if this session contains a mapping for the specified key. + * Returns true if this session contains a mapping for the specified key. * * @param key key whose presence in this session is to be tested - * @return true if this session contains a mapping for the specified key + * @return true if this session contains a mapping for the specified key */ public boolean has(Object key) { return data.containsKey(key); } /** - * Returns true if this session contains no mapping for the specified key. + * Returns true if this session contains no mapping for the specified key. * * @param key key whose presence in this session is to be tested - * @return true if this session contains no mapping for the specified key + * @return true if this session contains no mapping for the specified key */ public boolean hasNot(Object key) { return !data.containsKey(key); } /** - * Returns true if this session contains a value for the specified key and value is equal with given. + * Returns true if this session contains a value for the specified key and value is equal with given. * * @param key key whose presence in this session is to be tested * @param value value whose presence in this session is to be tested - * @return true if this session contains a value for the specified key and value is equal with given. + * @return true if this session contains a value for the specified key and value is equal with given. */ public boolean hasAndEqual(Object key, Object value) { final Object foundValue = data.get(key); diff --git a/core/src/main/java/spotty/common/stream/output/SpottyFixedByteOutputStream.java b/core/src/main/java/spotty/common/stream/output/SpottyFixedByteOutputStream.java index a79cf70..58fbaf2 100644 --- a/core/src/main/java/spotty/common/stream/output/SpottyFixedByteOutputStream.java +++ b/core/src/main/java/spotty/common/stream/output/SpottyFixedByteOutputStream.java @@ -97,7 +97,15 @@ public boolean isFull() { return size == limit; } + public byte[] sourceData() { + return data; + } + public byte[] toByteArray() { + if (size == data.length) { + return data; + } + return Arrays.copyOf(data, size); } @@ -115,7 +123,7 @@ public void capacity(int capacity) { } if (data.length != capacity) { - byte[] d = new byte[capacity]; + final byte[] d = new byte[capacity]; this.size = min(size, capacity); if (size > 0) { diff --git a/core/src/main/java/spotty/server/Server.java b/core/src/main/java/spotty/server/Server.java index 908b8f5..e501a9c 100644 --- a/core/src/main/java/spotty/server/Server.java +++ b/core/src/main/java/spotty/server/Server.java @@ -260,6 +260,7 @@ private void accept(SelectionKey acceptKey) throws IOException { private void registerConnection(Connection connection, Selector selector) { final SelectionKey key = connection.register(selector); if (key == null) { + connection.close(); return; } diff --git a/core/src/main/java/spotty/server/connection/Connection.java b/core/src/main/java/spotty/server/connection/Connection.java index c48d381..d9b06dc 100644 --- a/core/src/main/java/spotty/server/connection/Connection.java +++ b/core/src/main/java/spotty/server/connection/Connection.java @@ -24,7 +24,7 @@ import spotty.common.http.HttpProtocol; import spotty.common.request.SpottyDefaultRequest; import spotty.common.request.params.QueryParams; -import spotty.common.response.ResponseWriter; +import spotty.common.response.ResponseHeadersWriter; import spotty.common.response.SpottyResponse; import spotty.common.state.StateHandlerGraph; import spotty.common.state.StateHandlerGraph.GraphFilter; @@ -81,7 +81,8 @@ import static spotty.server.connection.state.ConnectionState.REQUEST_HANDLING; import static spotty.server.connection.state.ConnectionState.REQUEST_READY; import static spotty.server.connection.state.ConnectionState.RESPONSE_WRITE_COMPLETED; -import static spotty.server.connection.state.ConnectionState.RESPONSE_WRITING; +import static spotty.server.connection.state.ConnectionState.RESPONSE_WRITING_BODY; +import static spotty.server.connection.state.ConnectionState.RESPONSE_WRITING_HEADERS; public final class Connection extends StateMachine implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(Connection.class); @@ -98,7 +99,7 @@ public final class Connection extends StateMachine implements C @VisibleForTesting final SpottyResponse response = new SpottyResponse(); - private final ResponseWriter responseWriter = new ResponseWriter(); + private final SpottyByteArrayOutputStream responseHeadersBuffer = new SpottyByteArrayOutputStream(DEFAULT_BUFFER_SIZE); private final SpottyByteArrayOutputStream line = new SpottyByteArrayOutputStream(DEFAULT_LINE_SIZE); private final SpottyFixedByteOutputStream body = new SpottyFixedByteOutputStream(DEFAULT_BUFFER_SIZE); @@ -110,7 +111,9 @@ public final class Connection extends StateMachine implements C private final int maxRequestBodySize; private ByteBuffer readBuffer; private RequestHandler requestHandler; - private ByteBuffer writeBuffer; + + private ByteBuffer headersByteBuffer; + private ByteBuffer bodyByteBuffer; public Connection(SpottySocket socket, RequestHandler requestHandler, @@ -194,18 +197,14 @@ public void after() { .node(REQUEST_READY).apply(this::requestHandling) .entry(READY_TO_WRITE).apply(this::readyToWrite) - .node(RESPONSE_WRITING).apply(this::writeResponse) + .node(RESPONSE_WRITING_HEADERS).apply(this::writeResponseHeaders) + .node(RESPONSE_WRITING_BODY).apply(this::writeResponseBody) .node(RESPONSE_WRITE_COMPLETED).apply(this::responseWriteCompleted) ; } public SelectionKey register(Selector selector) { - final SelectionKey key = exceptionHandler(() -> socket.register(selector, OP_CONNECT, this)); - if (key == null) { - close(); - } - - return key; + return exceptionHandler(() -> socket.register(selector, OP_CONNECT, this)); } public void markDataRemaining() { @@ -500,6 +499,7 @@ private boolean requestHandling() { private final Runnable handlerRequest = () -> { exceptionHandler(actionExceptionHandler); + request.reset(); changeState(READY_TO_WRITE); }; @@ -511,22 +511,58 @@ private boolean requestHandling() { private boolean readyToWrite() { checkStateIs(READY_TO_WRITE); - final byte[] data = responseWriter.write(response); - this.writeBuffer = ByteBuffer.wrap(data); + ResponseHeadersWriter.write(responseHeadersBuffer, response); + if (headersByteBuffer == null || headersByteBuffer.capacity() != responseHeadersBuffer.capacity()) { + // wrap by link, changing byte[] is affecting writeHeadersBuffer + headersByteBuffer = ByteBuffer.wrap( + responseHeadersBuffer.sourceData(), + 0, + responseHeadersBuffer.size() + ); + } else { + headersByteBuffer + .position(0) + .limit(responseHeadersBuffer.size()) + ; + } + + if (response.body() != null) { + this.bodyByteBuffer = ByteBuffer.wrap(response.body()); + } + + return changeState(RESPONSE_WRITING_HEADERS); + } + + private boolean writeResponseHeaders() { + checkStateIs(RESPONSE_WRITING_HEADERS); + + try { + socket.write(headersByteBuffer); + if (!headersByteBuffer.hasRemaining()) { + return changeState(RESPONSE_WRITING_BODY); + } + } catch (IOException e) { + LOG.error("response write headers error", e); + close(); + } - return changeState(RESPONSE_WRITING); + return false; } - private boolean writeResponse() throws SpottyHttpException { - checkStateIs(RESPONSE_WRITING); + private boolean writeResponseBody() throws SpottyHttpException { + checkStateIs(RESPONSE_WRITING_BODY); + + if (bodyByteBuffer == null) { + return changeState(RESPONSE_WRITE_COMPLETED); + } try { - socket.write(writeBuffer); - if (!writeBuffer.hasRemaining()) { + socket.write(bodyByteBuffer); + if (!bodyByteBuffer.hasRemaining()) { return changeState(RESPONSE_WRITE_COMPLETED); } } catch (IOException e) { - LOG.error("response write error", e); + LOG.error("response write body error", e); close(); } @@ -543,7 +579,6 @@ private boolean responseWriteCompleted() { } } finally { resetResponse(); - request.reset(); } changeState(READY_TO_READ); @@ -557,8 +592,9 @@ private boolean responseWriteCompleted() { } private void resetResponse() { - this.writeBuffer = null; this.response.reset(); + this.responseHeadersBuffer.reset(); + this.bodyByteBuffer = null; } private void parseHeadLine(String line) { diff --git a/core/src/main/java/spotty/server/connection/socket/SSLSocket.java b/core/src/main/java/spotty/server/connection/socket/SSLSocket.java index 25f6274..28cc546 100644 --- a/core/src/main/java/spotty/server/connection/socket/SSLSocket.java +++ b/core/src/main/java/spotty/server/connection/socket/SSLSocket.java @@ -125,38 +125,37 @@ public int read(ByteBuffer dst) throws IOException { @Override public int write(ByteBuffer src) throws IOException { + myNetBuffer.clear(); + myAppBuffer.clear(); + + bufferCopyRemaining(src, myAppBuffer); + + myAppBuffer.flip(); + + Status status; int writeData = 0; - while (src.hasRemaining()) { - myNetBuffer.clear(); - myAppBuffer.clear(); - - bufferCopyRemaining(src, myAppBuffer); - - myAppBuffer.flip(); - - Status status; - do { - status = wrap(myAppBuffer, myNetBuffer); - LOG.debug("write.status {}", status); - switch (status) { - case OK: - myNetBuffer.flip(); - while (myNetBuffer.hasRemaining()) { - writeData += socketChannel.write(myNetBuffer); - } - break; - case BUFFER_OVERFLOW: - myNetBuffer = enlargePacketBuffer(myNetBuffer); - break; - case BUFFER_UNDERFLOW: - myAppBuffer = handleBufferUnderflow(myAppBuffer); - break; - case CLOSED: - close(); - return writeData; - } - } while (status != OK); - } + + do { + status = wrap(myAppBuffer, myNetBuffer); + LOG.debug("write.status {}", status); + switch (status) { + case OK: + myNetBuffer.flip(); + while (myNetBuffer.hasRemaining()) { + writeData += socketChannel.write(myNetBuffer); + } + break; + case BUFFER_OVERFLOW: + myNetBuffer = enlargePacketBuffer(myNetBuffer); + break; + case BUFFER_UNDERFLOW: + myAppBuffer = handleBufferUnderflow(myAppBuffer); + break; + case CLOSED: + close(); + return writeData; + } + } while (status != OK); return writeData; } diff --git a/core/src/main/java/spotty/server/connection/state/ConnectionState.java b/core/src/main/java/spotty/server/connection/state/ConnectionState.java index 5877a92..d377bab 100644 --- a/core/src/main/java/spotty/server/connection/state/ConnectionState.java +++ b/core/src/main/java/spotty/server/connection/state/ConnectionState.java @@ -34,7 +34,8 @@ public enum ConnectionState { REQUEST_READY, REQUEST_HANDLING, READY_TO_WRITE, - RESPONSE_WRITING, + RESPONSE_WRITING_HEADERS, + RESPONSE_WRITING_BODY, RESPONSE_WRITE_COMPLETED, CLOSED; diff --git a/core/src/main/java/spotty/server/worker/ReactorWorker.java b/core/src/main/java/spotty/server/worker/ReactorWorker.java index a333723..ead881e 100644 --- a/core/src/main/java/spotty/server/worker/ReactorWorker.java +++ b/core/src/main/java/spotty/server/worker/ReactorWorker.java @@ -20,8 +20,8 @@ import java.io.Closeable; import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -38,7 +38,7 @@ public ReactorWorker(int minWorkers, int maxWorkers, long keepAliveTime, TimeUni maxWorkers, keepAliveTime, notNull("timeUnit", timeUnit), - new LinkedBlockingQueue<>(100), + new SynchronousQueue<>(), threadPool("spotty-reactor"), new RejectedHandler() ); diff --git a/core/src/test/groovy/spotty/common/response/ResponseWriterTest.groovy b/core/src/test/groovy/spotty/common/response/ResponseHeadersWriterTest.groovy similarity index 66% rename from core/src/test/groovy/spotty/common/response/ResponseWriterTest.groovy rename to core/src/test/groovy/spotty/common/response/ResponseHeadersWriterTest.groovy index c05d782..c52c082 100644 --- a/core/src/test/groovy/spotty/common/response/ResponseWriterTest.groovy +++ b/core/src/test/groovy/spotty/common/response/ResponseHeadersWriterTest.groovy @@ -2,12 +2,13 @@ package spotty.common.response import spock.lang.Specification import spotty.common.request.WebRequestTestData +import spotty.common.stream.output.SpottyByteArrayOutputStream -class ResponseWriterTest extends Specification implements WebRequestTestData { +class ResponseHeadersWriterTest extends Specification implements WebRequestTestData { def "should write response correctly"() { given: - var responseWriter = new ResponseWriter() + var data = new SpottyByteArrayOutputStream() var content = "hello".getBytes() var request = aSpottyRequest() .contentLength(content.length) @@ -18,11 +19,10 @@ class ResponseWriterTest extends Specification implements WebRequestTestData { .cookie("title", "title") when: - var data = responseWriter.write(response) - var responseString = new String(data) + ResponseHeadersWriter.write(data, response) then: - responseString == expectedResponse + data.toString() == expectedResponse } def expectedResponse = """ @@ -31,8 +31,6 @@ class ResponseWriterTest extends Specification implements WebRequestTestData { content-type: text/plain set-cookie: name=name set-cookie: title=title - - hello - """.stripIndent(true).trim() + """.stripIndent(true).trim() + "\n\n" } diff --git a/core/src/test/groovy/spotty/common/stream/output/SpottyFixedByteOutputStreamTest.groovy b/core/src/test/groovy/spotty/common/stream/output/SpottyFixedByteOutputStreamTest.groovy index 629c519..4d7c6d9 100644 --- a/core/src/test/groovy/spotty/common/stream/output/SpottyFixedByteOutputStreamTest.groovy +++ b/core/src/test/groovy/spotty/common/stream/output/SpottyFixedByteOutputStreamTest.groovy @@ -148,4 +148,28 @@ class SpottyFixedByteOutputStreamTest extends Specification { "he" | 2 } + def "should return original byte[] data when stream is full"() { + given: + var stream = new SpottyFixedByteOutputStream(12) + + when: + stream.print("hello world!") + + then: + // compare by link + stream.sourceData().equals(stream.toByteArray()) + } + + def "should return copy byte[] data when stream is not full"() { + given: + var stream = new SpottyFixedByteOutputStream(15) + + when: + stream.print("hello world!") + + then: + // compare by link + stream.sourceData().equals(stream.toByteArray()) == false + } + } diff --git a/core/src/test/groovy/spotty/server/connection/ConnectionTest.groovy b/core/src/test/groovy/spotty/server/connection/ConnectionTest.groovy index a365098..b6ec336 100644 --- a/core/src/test/groovy/spotty/server/connection/ConnectionTest.groovy +++ b/core/src/test/groovy/spotty/server/connection/ConnectionTest.groovy @@ -5,8 +5,9 @@ import spotty.common.exception.SpottyHttpException import spotty.common.exception.SpottyStreamException import spotty.common.exception.SpottyValidationException import spotty.common.request.WebRequestTestData -import spotty.common.response.ResponseWriter +import spotty.common.response.ResponseHeadersWriter import spotty.common.response.SpottyResponse +import spotty.common.stream.output.SpottyByteArrayOutputStream import spotty.server.connection.socket.SocketFactory import spotty.server.handler.EchoRequestHandler import spotty.server.registry.exception.ExceptionHandlerRegistry @@ -29,10 +30,10 @@ import static spotty.server.connection.state.ConnectionState.READY_TO_WRITE class ConnectionTest extends Specification implements WebRequestTestData { private def socketFactory = new SocketFactory() - private def responseWriter = new ResponseWriter() private def exceptionService = new ExceptionHandlerRegistry() private def reactorWorker = new ReactorWorker(1, 1, 10, SECONDS) private def maxBodyLimit = 10 * 1024 * 1024 // 10Mb + private def data = new SpottyByteArrayOutputStream() def setup() { exceptionService.register(SpottyHttpException.class, (exception, request, response) -> { @@ -84,7 +85,12 @@ class ConnectionTest extends Specification implements WebRequestTestData { socket.flip() var request = aSpottyRequest() - var expectedResponse = new String(responseWriter.write(aSpottyResponse(request))) + var response = aSpottyResponse(request) + + ResponseHeadersWriter.write(data, response) + data.write(response.body()) + + var expectedResponse = data.toString() var connection = new Connection(socketFactory.createSocket(socket), new EchoRequestHandler(), reactorWorker, exceptionService, maxBodyLimit, fullRequest.length()) connection.markReadyToRead() @@ -98,10 +104,10 @@ class ConnectionTest extends Specification implements WebRequestTestData { connection.handle() socket.flip() - var response = new String(socket.getAllBytes()) + var actualResponse = new String(socket.getAllBytes()) then: - response == expectedResponse + actualResponse == expectedResponse } def "should throw exception when socket is blocking"() { @@ -190,7 +196,10 @@ class ConnectionTest extends Specification implements WebRequestTestData { .contentType("text/plain") .body("some message") - var expectedResult = new String(responseWriter.write(response)) + ResponseHeadersWriter.write(data, response) + data.write(response.body()) + + var expectedResult = data.toString() var socket = new SocketChannelStub() socket.configureBlocking(false) @@ -233,7 +242,10 @@ class ConnectionTest extends Specification implements WebRequestTestData { .addHeader(CONNECTION, CLOSE.code) .body(MOVED_PERMANENTLY.statusMessage) - var expectedResult = new String(responseWriter.write(response)) + ResponseHeadersWriter.write(data, response) + data.write(response.body()) + + var expectedResult = data.toString() var socket = new SocketChannelStub() socket.configureBlocking(false) diff --git a/core/src/testIntegration/groovy/spotty/server/SpottyWrongRawRequestSpec.groovy b/core/src/testIntegration/groovy/spotty/server/SpottyWrongRawRequestSpec.groovy index 55b12f5..7077fcb 100644 --- a/core/src/testIntegration/groovy/spotty/server/SpottyWrongRawRequestSpec.groovy +++ b/core/src/testIntegration/groovy/spotty/server/SpottyWrongRawRequestSpec.groovy @@ -63,6 +63,7 @@ class SpottyWrongRawRequestSpec extends AppTestContext { var socket = new Socket(SPOTTY.host(), SPOTTY.port()) var writer = new PrintWriter(socket.getOutputStream()) var inputStream = socket.getInputStream() + var errorMessage = "invalid request head line: POST HTTP/1.1" when: writer.println("POST HTTP/1.1") @@ -70,9 +71,10 @@ class SpottyWrongRawRequestSpec extends AppTestContext { writer.println() writer.flush() - var buff = new byte[128] + var buff = new byte[256] var read = inputStream.read(buff) - var result = new String(buff, 0, read) + inputStream.read(buff, read, errorMessage.length()) + var result = new String(buff).trim() then: result == """ @@ -81,7 +83,7 @@ class SpottyWrongRawRequestSpec extends AppTestContext { content-type: text/plain connection: close - invalid request head line: POST HTTP/1.1 + $errorMessage """.stripIndent().trim() } @@ -107,6 +109,7 @@ class SpottyWrongRawRequestSpec extends AppTestContext { var socket = new Socket(SPOTTY.host(), SPOTTY.port()) var writer = new PrintWriter(socket.getOutputStream()) var inputStream = socket.getInputStream() + var errorMessage = "Spotty is supports HTTP/1.0, HTTP/1.1 protocols only" when: writer.println("POST / HTTP/2.0") @@ -116,7 +119,8 @@ class SpottyWrongRawRequestSpec extends AppTestContext { var buff = new byte[256] var read = inputStream.read(buff) - var result = new String(buff, 0, read) + inputStream.read(buff, read, errorMessage.length()) + var result = new String(buff).trim() then: result == """ @@ -124,8 +128,8 @@ class SpottyWrongRawRequestSpec extends AppTestContext { content-length: 52 content-type: text/plain connection: close - - Spotty is supports HTTP/1.0, HTTP/1.1 protocols only + + $errorMessage """.stripIndent().trim() } @@ -134,6 +138,7 @@ class SpottyWrongRawRequestSpec extends AppTestContext { var socket = new Socket(SPOTTY.host(), SPOTTY.port()) var writer = new PrintWriter(socket.getOutputStream()) var inputStream = socket.getInputStream() + var errorMessage = "invalid header line: wrong header" when: writer.println("POST / HTTP/1.1") @@ -143,7 +148,8 @@ class SpottyWrongRawRequestSpec extends AppTestContext { var buff = new byte[256] var read = inputStream.read(buff) - var result = new String(buff, 0, read) + inputStream.read(buff, read, errorMessage.length()) + var result = new String(buff).trim() then: result == """ @@ -152,7 +158,7 @@ class SpottyWrongRawRequestSpec extends AppTestContext { content-type: text/plain connection: close - invalid header line: wrong header + $errorMessage """.stripIndent().trim() }