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

Use ResponseLifecycle from core http-server #837

Draft
wants to merge 1 commit into
base: 4.13.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
@@ -0,0 +1,91 @@
/*
* Copyright 2017-2024 original 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
*
* https://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.micronaut.http.poja.apache;

import io.micronaut.http.HttpHeaders;
import io.micronaut.http.server.exceptions.HttpServerException;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.impl.io.ChunkedOutputStream;
import org.apache.hc.core5.http.impl.io.ContentLengthOutputStream;
import org.apache.hc.core5.http.impl.io.DefaultHttpResponseWriter;
import org.apache.hc.core5.http.impl.io.SessionOutputBufferImpl;
import org.apache.hc.core5.http.io.SessionOutputBuffer;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;

final class ApacheResponseContext implements Closeable {
boolean connectionClose = false;
ApacheServletHttpResponse<?> primaryResponse;

private final SessionOutputBuffer outputBuffer;
private final OutputStream out;
private OutputStream bodyStream;

ApacheResponseContext(ApacheServletConfiguration configuration, OutputStream out) {
this.out = out;
outputBuffer = new SessionOutputBufferImpl(configuration.outputBufferSize());
}

boolean isCommitted() {
return bodyStream != null;
}

OutputStream commit(ClassicHttpResponse headers) throws IOException {
if (isCommitted()) {
throw new IllegalStateException("Response has already been committed");
}

headers.removeHeaders(HttpHeaders.TRANSFER_ENCODING);
Header contentLengthStr = headers.getFirstHeader(HttpHeaders.CONTENT_LENGTH);
long contentLength = contentLengthStr == null ? -1 : Long.parseLong(contentLengthStr.getValue());
if (contentLength < 0) {
headers.removeHeaders(HttpHeaders.CONTENT_LENGTH);
headers.addHeader(HttpHeaders.TRANSFER_ENCODING, "chunked");
}

Header connection = headers.getFirstHeader(HttpHeaders.CONNECTION);
if (connection != null && connection.getValue().equalsIgnoreCase("close")) {
connectionClose = true;
}

DefaultHttpResponseWriter responseWriter = new DefaultHttpResponseWriter();
try {
responseWriter.write(headers, outputBuffer, out);
} catch (HttpException e) {
throw new HttpServerException("Could not write response headers", e);
}

OutputStream s = contentLength < 0 ? new ChunkedOutputStream(outputBuffer, out, 0) : new ContentLengthOutputStream(outputBuffer, out, contentLength);
bodyStream = s;
return s;
}

@Override
public void close() throws IOException {
if (bodyStream != null) {
bodyStream.close();
}
outputBuffer.flush(out);

if (connectionClose) {
out.close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,24 @@

import io.micronaut.context.ApplicationContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.io.buffer.ByteArrayBufferFactory;
import io.micronaut.core.io.buffer.ByteBufferFactory;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.http.poja.PojaHttpServerlessApplication;
import io.micronaut.http.poja.apache.exception.ApacheServletBadRequestException;
import io.micronaut.http.server.exceptions.HttpServerException;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.runtime.ApplicationConfiguration;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.servlet.http.ByteArrayBufferFactory;
import io.micronaut.servlet.http.ServletHttpHandler;
import jakarta.inject.Singleton;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.impl.io.DefaultHttpResponseWriter;
import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl;
import org.apache.hc.core5.http.impl.io.SessionOutputBufferImpl;
import org.apache.hc.core5.http.io.SessionInputBuffer;
import org.apache.hc.core5.http.io.SessionOutputBuffer;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;

/**
Expand Down Expand Up @@ -78,46 +71,34 @@ public ApacheServerlessApplication(ApplicationContext applicationContext,
}

@Override
protected void handleSingleRequest(
protected boolean handleSingleRequest(
ServletHttpHandler<ApacheServletHttpRequest<?>, ApacheServletHttpResponse<?>> servletHttpHandler,
InputStream in,
OutputStream out
) throws IOException {
ApacheServletHttpResponse<?> response = new ApacheServletHttpResponse<>(conversionService);
try {
// The buffer is initialized only once
if (sessionInputBuffer == null) {
sessionInputBuffer = new SessionInputBufferImpl(configuration.inputBufferSize());
try (ApacheResponseContext responseContext = new ApacheResponseContext(configuration, out)) {
try {
// The buffer is initialized only once
if (sessionInputBuffer == null) {
sessionInputBuffer = new SessionInputBufferImpl(configuration.inputBufferSize());
}
ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>(
in, responseContext, sessionInputBuffer, conversionService, codecRegistry, ioExecutor, byteBufferFactory
);
servletHttpHandler.service(exchange);
if (!responseContext.isCommitted()) {
responseContext.primaryResponse.getOutputStream(); // this causes the commit
}
} catch (Exception e) {
if (!responseContext.isCommitted()) {
try (OutputStream os = responseContext.commit(new BasicClassicHttpResponse(HttpStatus.BAD_REQUEST.getCode()))) {
os.write(e.getMessage().getBytes(StandardCharsets.UTF_8));
}
}
throw e;
}
ApacheServletHttpRequest exchange = new ApacheServletHttpRequest<>(
in, sessionInputBuffer, conversionService, codecRegistry, ioExecutor, byteBufferFactory, response, configuration
);
servletHttpHandler.service(exchange);
} catch (ApacheServletBadRequestException e) {
response.status(HttpStatus.BAD_REQUEST);
response.contentType(MediaType.TEXT_PLAIN_TYPE);
response.getOutputStream().write(e.getMessage().getBytes());
writeResponse(response.getNativeResponse(), out);
throw e;
return !responseContext.connectionClose;
}
writeResponse(response.getNativeResponse(), out);
}

private void writeResponse(ClassicHttpResponse response, OutputStream out) throws IOException {
SessionOutputBuffer buffer = new SessionOutputBufferImpl(configuration.outputBufferSize());
DefaultHttpResponseWriter responseWriter = new DefaultHttpResponseWriter();
try {
responseWriter.write(response, buffer, out);
} catch (HttpException e) {
throw new HttpServerException("Could not write response body", e);
}
buffer.flush(out);

HttpEntity entity = response.getEntity();
if (entity != null) {
entity.writeTo(out);
}
out.flush();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.micronaut.http.MutableHttpParameters;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.body.ByteBody;
import io.micronaut.http.body.ByteBodyFactory;
import io.micronaut.http.body.stream.InputStreamByteBody;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.http.cookie.Cookie;
Expand All @@ -35,6 +36,7 @@
import io.micronaut.http.poja.util.MultiValueHeaders;
import io.micronaut.http.poja.util.MultiValuesQueryParameters;
import io.micronaut.http.simple.cookies.SimpleCookies;
import io.micronaut.servlet.http.ServletHttpResponse;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
Expand Down Expand Up @@ -73,6 +75,7 @@ public final class ApacheServletHttpRequest<B> extends PojaHttpRequest<B, Classi
private static final String TRANSFER_ENCODING_CHUNKED = "chunked";

private final ClassicHttpRequest request;
private final ApacheResponseContext responseContext;

private final HttpMethod method;
private URI uri;
Expand All @@ -82,28 +85,30 @@ public final class ApacheServletHttpRequest<B> extends PojaHttpRequest<B, Classi

private final ByteBody byteBody;

private ApacheServletHttpResponse<?> primaryResponse;

/**
* Create an Apache-based request.
*
* @param inputStream The input stream
* @param responseContext The response context
* @param sessionInputBuffer Input buffer for parsing
* @param conversionService The conversion service
* @param codecRegistry The media codec registry
* @param ioExecutor The executor service
* @param byteBufferFactory The byte buffer factory
* @param response The response
* @param configuration The configuration
*/
public ApacheServletHttpRequest(
InputStream inputStream,
ApacheResponseContext responseContext,
SessionInputBuffer sessionInputBuffer,
ConversionService conversionService,
MediaTypeCodecRegistry codecRegistry,
ExecutorService ioExecutor,
ByteBufferFactory<?, ?> byteBufferFactory,
ApacheServletHttpResponse<?> response,
ApacheServletConfiguration configuration
ByteBufferFactory<?, ?> byteBufferFactory
) {
super(conversionService, codecRegistry, response);
super(conversionService, codecRegistry);
this.responseContext = responseContext;
DefaultHttpRequestParser parser = new DefaultHttpRequestParser();

try {
Expand All @@ -125,12 +130,20 @@ public ApacheServletHttpRequest(
queryParameters = parseQueryParameters(uri, conversionService);
cookies = parseCookies(request, conversionService);

Header connection = request.getFirstHeader(HttpHeaders.CONNECTION);
if (connection != null && connection.getValue().equalsIgnoreCase("close")) {
responseContext.connectionClose = true;
}

long contentLength = getContentLength();
if (!getMethod().permitsRequestBody()) {
contentLength = 0;
}
OptionalLong optionalContentLength = contentLength >= 0 ? OptionalLong.of(contentLength) : OptionalLong.empty();
InputStream bodyStream = createBodyStream(inputStream, contentLength, sessionInputBuffer);
byteBody = InputStreamByteBody.create(
bodyStream, optionalContentLength, ioExecutor, byteBufferFactory
);
bodyStream, optionalContentLength, ioExecutor, ByteBodyFactory.createDefault(byteBufferFactory));
primaryResponse = new ApacheServletHttpResponse<>(responseContext, conversionService);
}

/**
Expand All @@ -156,6 +169,18 @@ private InputStream createBodyStream(InputStream inputStream, long contentLength
return bodyStream;
}

@Override
public ServletHttpResponse<ClassicHttpResponse, ?> getResponse() {
return primaryResponse;
}

@Override
public ServletHttpResponse<ClassicHttpResponse, ?> createResponse() {
ApacheServletHttpResponse<Object> r = new ApacheServletHttpResponse<>(responseContext, conversionService);
primaryResponse = r;
return r;
}

@Override
public ClassicHttpRequest getNativeRequest() {
return request;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,20 @@
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.value.MutableConvertibleValues;
import io.micronaut.core.convert.value.MutableConvertibleValuesMap;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MutableHttpHeaders;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.poja.PojaHttpResponse;
import io.micronaut.http.simple.SimpleHttpHeaders;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

/**
Expand All @@ -48,11 +45,12 @@
* @since 4.10.0
*/
@Internal
public final class ApacheServletHttpResponse<T> extends PojaHttpResponse<T, ClassicHttpResponse> {
final class ApacheServletHttpResponse<T> extends PojaHttpResponse<T, ClassicHttpResponse> {

private final ApacheResponseContext responseContext;

private int code = HttpStatus.OK.getCode();
private String reasonPhrase = HttpStatus.OK.getReason();
private final ByteArrayOutputStream out = new ByteArrayOutputStream();

private final SimpleHttpHeaders headers;
private final MutableConvertibleValues<Object> attributes = new MutableConvertibleValuesMap<>();
Expand All @@ -63,35 +61,27 @@ public final class ApacheServletHttpResponse<T> extends PojaHttpResponse<T, Clas
*
* @param conversionService The conversion service
*/
public ApacheServletHttpResponse(ConversionService conversionService) {
ApacheServletHttpResponse(ApacheResponseContext responseContext, ConversionService conversionService) {
this.responseContext = responseContext;
this.headers = new SimpleHttpHeaders(conversionService);
responseContext.primaryResponse = this;
}

@Override
public ClassicHttpResponse getNativeResponse() {
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(out.size()));
if ("chunked".equalsIgnoreCase(headers.get(HttpHeaders.TRANSFER_ENCODING))) {
headers.remove(HttpHeaders.TRANSFER_ENCODING);
}

BasicClassicHttpResponse response = new BasicClassicHttpResponse(code, reasonPhrase);
headers.forEachValue(response::addHeader);
ContentType contentType = headers.getContentType().map(ContentType::parse)
.orElse(ContentType.APPLICATION_JSON);
ByteArrayEntity body = new ByteArrayEntity(out.toByteArray(), contentType);
response.setEntity(body);
return response;
}

@Override
public OutputStream getOutputStream() throws IOException {
return out;
return responseContext.commit(getNativeResponse());
}

@Override
public BufferedWriter getWriter() throws IOException {
return new BufferedWriter(new PrintWriter(out));
return new BufferedWriter(new OutputStreamWriter(getOutputStream(), StandardCharsets.UTF_8));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ class SimpleServerFailedParseSpec extends Specification {
then:
response == SimpleServerSpec.unindent("""
HTTP/1.1 400 Bad Request\r
Content-Length: 32\r
Content-Type: text/plain\r
Transfer-Encoding: chunked\r
\r
HTTP request could not be parsed""")
20\r
HTTP request could not be parsed\r
0\r
\r
""")
}

}
Loading
Loading