diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index da4ffac8cfe9..68e693375d34 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1827,6 +1827,22 @@ public static String encodeBasicAuth(String username, String password, @Nullable return new String(encodedBytes, charset); } + /** + * Remove the well-known {@code "Content-*"} HTTP headers from the given instance. + *

Such headers should be cleared, if possible, from the response if the intended + * body can't be written due to errors. + * @since 5.2.3 + */ + public static void clearContentHeaders(HttpHeaders headers) { + headers.remove(HttpHeaders.CONTENT_DISPOSITION); + headers.remove(HttpHeaders.CONTENT_ENCODING); + headers.remove(HttpHeaders.CONTENT_LANGUAGE); + headers.remove(HttpHeaders.CONTENT_LENGTH); + headers.remove(HttpHeaders.CONTENT_LOCATION); + headers.remove(HttpHeaders.CONTENT_RANGE); + headers.remove(HttpHeaders.CONTENT_TYPE); + } + // Package-private: used in ResponseCookie static String formatDate(long date) { Instant instant = Instant.ofEpochMilli(date); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index d5378f058e25..e53366e27643 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -181,21 +181,22 @@ public final Mono writeWith(Publisher body) { if (body instanceof Mono) { return ((Mono) body).flatMap(buffer -> doCommit(() -> writeWithInternal(Mono.just(buffer))) - .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)); + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)) + .doOnError(t -> clearContentHeaders()); } return new ChannelSendOperator<>(body, inner -> doCommit(() -> writeWithInternal(inner))) - .doOnError(t -> removeContentLength()); + .doOnError(t -> clearContentHeaders()); } @Override public final Mono writeAndFlushWith(Publisher> body) { return new ChannelSendOperator<>(body, inner -> doCommit(() -> writeAndFlushWithInternal(inner))) - .doOnError(t -> removeContentLength()); + .doOnError(t -> clearContentHeaders()); } - private void removeContentLength() { + private void clearContentHeaders() { if (!this.isCommitted()) { - this.getHeaders().remove(HttpHeaders.CONTENT_LENGTH); + HttpHeaders.clearContentHeaders(this.getHeaders()); } } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index acc7b4bd71d8..c8a8552dd346 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.core.io.buffer.DefaultDataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; import static org.assertj.core.api.Assertions.assertThat; @@ -37,11 +38,12 @@ /** * @author Rossen Stoyanchev * @author Sebastien Deleuze + * @author Brian Clozel */ public class ServerHttpResponseTests { @Test - public void writeWith() throws Exception { + void writeWith() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).block(); @@ -56,7 +58,7 @@ public void writeWith() throws Exception { } @Test // SPR-14952 - public void writeAndFlushWithFluxOfDefaultDataBuffer() throws Exception { + void writeAndFlushWithFluxOfDefaultDataBuffer() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); Flux> flux = Flux.just(Flux.just(wrap("foo"))); response.writeAndFlushWith(flux).block(); @@ -70,21 +72,35 @@ public void writeAndFlushWithFluxOfDefaultDataBuffer() throws Exception { } @Test - public void writeWithError() throws Exception { - TestServerHttpResponse response = new TestServerHttpResponse(); - response.getHeaders().setContentLength(12); + void writeWithFluxError() throws Exception { IllegalStateException error = new IllegalStateException("boo"); - response.writeWith(Flux.error(error)).onErrorResume(ex -> Mono.empty()).block(); + writeWithError(Flux.error(error)); + } + + @Test + void writeWithMonoError() throws Exception { + IllegalStateException error = new IllegalStateException("boo"); + writeWithError(Mono.error(error)); + } + + void writeWithError(Publisher body) throws Exception { + TestServerHttpResponse response = new TestServerHttpResponse(); + HttpHeaders headers = response.getHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set(HttpHeaders.CONTENT_ENCODING, "gzip"); + headers.setContentLength(12); + response.writeWith(body).onErrorResume(ex -> Mono.empty()).block(); assertThat(response.statusCodeWritten).isFalse(); assertThat(response.headersWritten).isFalse(); assertThat(response.cookiesWritten).isFalse(); - assertThat(response.getHeaders().containsKey(HttpHeaders.CONTENT_LENGTH)).isFalse(); + assertThat(headers).doesNotContainKeys(HttpHeaders.CONTENT_TYPE, HttpHeaders.CONTENT_LENGTH, + HttpHeaders.CONTENT_ENCODING); assertThat(response.body.isEmpty()).isTrue(); } @Test - public void setComplete() throws Exception { + void setComplete() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); response.setComplete().block(); @@ -95,7 +111,7 @@ public void setComplete() throws Exception { } @Test - public void beforeCommitWithComplete() throws Exception { + void beforeCommitWithComplete() throws Exception { ResponseCookie cookie = ResponseCookie.from("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> Mono.fromRunnable(() -> response.getCookies().add(cookie.getName(), cookie))); @@ -113,7 +129,7 @@ public void beforeCommitWithComplete() throws Exception { } @Test - public void beforeCommitActionWithSetComplete() throws Exception { + void beforeCommitActionWithSetComplete() throws Exception { ResponseCookie cookie = ResponseCookie.from("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> { @@ -130,7 +146,6 @@ public void beforeCommitActionWithSetComplete() throws Exception { } - private DefaultDataBuffer wrap(String a) { return new DefaultDataBufferFactory().wrap(ByteBuffer.wrap(a.getBytes(StandardCharsets.UTF_8))); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 4608df51ab31..a8f5a1efeac6 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -211,7 +211,7 @@ private Mono handleException(Throwable exception, HandlerMethod h // Success and error responses may use different content types exchange.getAttributes().remove(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!exchange.getResponse().isCommitted()) { - exchange.getResponse().getHeaders().remove(HttpHeaders.CONTENT_TYPE); + HttpHeaders.clearContentHeaders(exchange.getResponse().getHeaders()); } InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod);