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

Regression Writable in Filter #9287

Merged
merged 2 commits into from
May 22, 2023
Merged
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
Expand Up @@ -93,6 +93,7 @@
import io.micronaut.http.netty.body.ByteBufRawMessageBodyHandler;
import io.micronaut.http.netty.body.NettyJsonHandler;
import io.micronaut.http.netty.body.NettyJsonStreamHandler;
import io.micronaut.http.netty.body.NettyWritableBodyWriter;
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer;
import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse;
import io.micronaut.http.netty.stream.DelegateStreamedHttpRequest;
Expand Down Expand Up @@ -1802,7 +1803,8 @@ private static MediaTypeCodecRegistry createDefaultMediaTypeRegistry() {
}

private static MessageBodyHandlerRegistry createDefaultMessageBodyHandlerRegistry() {
ContextlessMessageBodyHandlerRegistry registry = new ContextlessMessageBodyHandlerRegistry(new ApplicationConfiguration(), NettyByteBufferFactory.DEFAULT, new ByteBufRawMessageBodyHandler());
ApplicationConfiguration applicationConfiguration = new ApplicationConfiguration();
ContextlessMessageBodyHandlerRegistry registry = new ContextlessMessageBodyHandlerRegistry(applicationConfiguration, NettyByteBufferFactory.DEFAULT, new ByteBufRawMessageBodyHandler(), new NettyWritableBodyWriter(applicationConfiguration));
JsonMapper mapper = JsonMapper.createDefault();
registry.add(MediaType.APPLICATION_JSON_TYPE, new NettyJsonHandler<>(mapper));
registry.add(MediaType.APPLICATION_JSON_STREAM_TYPE, new NettyJsonStreamHandler<>(mapper));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,52 @@
*/
package io.micronaut.http.netty.body;

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.io.Writable;
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.Headers;
import io.micronaut.core.type.MutableHeaders;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpHeaders;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.body.MessageBodyWriter;
import io.micronaut.http.body.RawMessageBodyHandler;
import io.micronaut.http.body.WritableBodyWriter;
import io.micronaut.http.codec.CodecException;
import io.micronaut.http.exceptions.MessageBodyException;
import io.micronaut.http.netty.NettyHttpHeaders;
import io.micronaut.runtime.ApplicationConfiguration;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;

@Replaces(WritableBodyWriter.class)
@Singleton
@Internal
final class NettyWritableBodyWriter implements NettyBodyWriter<Writable> {
private final WritableBodyWriter defaultWritable = new WritableBodyWriter();
@BootstrapContextCompatible
@Bean(typed = RawMessageBodyHandler.class)
public final class NettyWritableBodyWriter implements NettyBodyWriter<Writable>, RawMessageBodyHandler<Writable> {
private final WritableBodyWriter defaultWritable;

public NettyWritableBodyWriter(ApplicationConfiguration applicationConfiguration) {
defaultWritable = new WritableBodyWriter(applicationConfiguration);
}

@Override
public boolean isBlocking() {
Expand Down Expand Up @@ -78,4 +93,19 @@ public void writeTo(HttpRequest<?> request, MutableHttpResponse<Writable> outgoi
public void writeTo(Argument<Writable> type, MediaType mediaType, Writable object, MutableHeaders outgoingHeaders, OutputStream outputStream) throws CodecException {
defaultWritable.writeTo(type, mediaType, object, outgoingHeaders, outputStream);
}

@Override
public Publisher<? extends Writable> readChunked(Argument<Writable> type, MediaType mediaType, Headers httpHeaders, Publisher<ByteBuffer<?>> input) {
return defaultWritable.readChunked(type, mediaType, httpHeaders, input);
}

@Override
public Writable read(Argument<Writable> type, MediaType mediaType, Headers httpHeaders, InputStream inputStream) throws CodecException {
return defaultWritable.read(type, mediaType, httpHeaders, inputStream);
}

@Override
public Collection<? extends Class<?>> getTypes() {
return defaultWritable.getTypes();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2017-2023 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.server.tck.tests.bodywritable;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.io.Writable;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import io.micronaut.http.tck.AssertionUtils;
import io.micronaut.http.tck.BodyAssertion;
import io.micronaut.http.tck.HttpResponseAssertion;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

import static io.micronaut.http.tck.TestScenario.asserts;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SuppressWarnings({
"java:S5960", // We're allowed assertions, as these are used in tests only
"checkstyle:MissingJavadocType",
"checkstyle:DesignForExtension"
})
public class HtmlBodyWritableTest {
public static final String SPEC_NAME = "ControllerConstraintHandlerTest";
private static final HttpResponseAssertion ASSERTION = HttpResponseAssertion.builder()
.status(HttpStatus.OK)
.body(BodyAssertion.builder().body("<!DOCTYPE html><html></html>").equals())
.assertResponse(response -> {
assertTrue(response.getContentType().isPresent());
assertEquals(MediaType.TEXT_HTML_TYPE, response.getContentType().get());
}).build();

@Test
void htmlWritable() throws IOException {
asserts(SPEC_NAME,
HttpRequest.GET("/html/writable"),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, ASSERTION));
}

@Test
void htmlWritableMono() throws IOException {
asserts(SPEC_NAME,
HttpRequest.GET("/html/writablemono"),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, ASSERTION));
}

@Test
void htmlWritableFluxFilter() throws IOException {
asserts(SPEC_NAME,
HttpRequest.GET("/html/writablefluxfilter"),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, ASSERTION));
}

@Controller("/html")
@Requires(property = "spec.name", value = SPEC_NAME)
static class OnErrorMethodController {
@Get("/writable")
@Produces(MediaType.TEXT_HTML)
Writable index() {
return out -> out.write("<!DOCTYPE html><html></html>");
}

@Get("/writablemono")
@Produces(MediaType.TEXT_HTML)
Mono<Writable> indexmono() {
Writable writable = out -> out.write("<!DOCTYPE html><html></html>");
return Mono.just(writable);
}

@Get("/writablefluxfilter")
Map<String, Object> indexfluxfilter() {
return Collections.emptyMap();
}
}

@Requires(property = "spec.name", value = SPEC_NAME)
@Filter("/html/writablefluxfilter")
static class MockFilter implements HttpServerFilter {

@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
return Flux.from(chain.proceed(request))
.switchMap(response -> {
Writable writable = out -> out.write("<!DOCTYPE html><html></html>");
response.body(writable);
response.contentType(MediaType.TEXT_HTML);
return Flux.just(response);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,28 @@
*/
package io.micronaut.http.body;

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.io.Writable;
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.io.buffer.ReferenceCounted;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.Headers;
import io.micronaut.core.type.MutableHeaders;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.MediaType;
import io.micronaut.http.codec.CodecException;
import io.micronaut.runtime.ApplicationConfiguration;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Set;

/**
* Body writer for {@link Writable}s.
Expand All @@ -35,12 +46,25 @@
*/
@Singleton
@Experimental
public final class WritableBodyWriter implements MessageBodyWriter<Writable> {
@BootstrapContextCompatible
@Bean(typed = RawMessageBodyHandler.class)
public final class WritableBodyWriter implements RawMessageBodyHandler<Writable> {
private final ApplicationConfiguration applicationConfiguration;

public WritableBodyWriter(ApplicationConfiguration applicationConfiguration) {
this.applicationConfiguration = applicationConfiguration;
}

@Override
public boolean isBlocking() {
return true;
}

@Override
public Collection<? extends Class<?>> getTypes() {
return Set.of(Writable.class);
}

@Override
public void writeTo(Argument<Writable> type, MediaType mediaType, Writable object, MutableHeaders outgoingHeaders, OutputStream outputStream) throws CodecException {
if (mediaType != null && !outgoingHeaders.contains(HttpHeaders.CONTENT_TYPE)) {
Expand All @@ -53,4 +77,33 @@ public void writeTo(Argument<Writable> type, MediaType mediaType, Writable objec
throw new CodecException("Error writing body text: " + e.getMessage(), e);
}
}

private Writable read0(ByteBuffer<?> byteBuffer) {
String s = byteBuffer.toString(applicationConfiguration.getDefaultCharset());
if (byteBuffer instanceof ReferenceCounted rc) {
rc.release();
}
return w -> w.write(s);
}

@Override
public Publisher<? extends Writable> readChunked(Argument<Writable> type, MediaType mediaType, Headers httpHeaders, Publisher<ByteBuffer<?>> input) {
return Flux.from(input).map(this::read0);
}

@Override
public Writable read(Argument<Writable> type, MediaType mediaType, Headers httpHeaders, ByteBuffer<?> byteBuffer) throws CodecException {
return read0(byteBuffer);
}

@Override
public Writable read(Argument<Writable> type, MediaType mediaType, Headers httpHeaders, InputStream inputStream) throws CodecException {
String s;
try {
s = new String(inputStream.readAllBytes(), applicationConfiguration.getDefaultCharset());
} catch (IOException e) {
throw new CodecException("Failed to read InputStream", e);
}
return w -> w.write(s);
}
}