Browse Source

ResponseStatusException associated headers

A ResponseStatus exception now exposes extra method to return headers
for the response. This is used in ResponseStatusExceptionHandler to
apply the headers to the response.

Closes gh-23741
pull/27800/head
Rossen Stoyanchev 6 years ago
parent
commit
34cfbe5d26
  1. 15
      spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java
  2. 10
      spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java
  3. 17
      spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java
  4. 25
      spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java
  5. 26
      spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java

15
spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@ -19,12 +19,14 @@ package org.springframework.web.server; @@ -19,12 +19,14 @@ package org.springframework.web.server;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Exception for errors that fit response status 405 (method not allowed).
@ -55,6 +57,16 @@ public class MethodNotAllowedException extends ResponseStatusException { @@ -55,6 +57,16 @@ public class MethodNotAllowedException extends ResponseStatusException {
}
/**
* Return a Map with an "Allow" header.
* @since 5.1.11
*/
@Override
public Map<String, String> getHeaders() {
return Collections.singletonMap("Allow",
StringUtils.collectionToDelimitedString(this.supportedMethods, ", "));
}
/**
* Return the HTTP method for the failed request.
*/
@ -68,4 +80,5 @@ public class MethodNotAllowedException extends ResponseStatusException { @@ -68,4 +80,5 @@ public class MethodNotAllowedException extends ResponseStatusException {
public Set<HttpMethod> getSupportedMethods() {
return this.supportedMethods;
}
}

10
spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java

@ -18,6 +18,7 @@ package org.springframework.web.server; @@ -18,6 +18,7 @@ package org.springframework.web.server;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -51,6 +52,15 @@ public class NotAcceptableStatusException extends ResponseStatusException { @@ -51,6 +52,15 @@ public class NotAcceptableStatusException extends ResponseStatusException {
}
/**
* Return a Map with an "Accept" header.
* @since 5.1.11
*/
@Override
public Map<String, String> getHeaders() {
return Collections.singletonMap("Accept", MediaType.toString(this.supportedMediaTypes));
}
/**
* Return the list of supported content types in cases when the Accept
* header is parsed but not supported, or an empty list otherwise.

17
spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@ -16,6 +16,9 @@ @@ -16,6 +16,9 @@
package org.springframework.web.server;
import java.util.Collections;
import java.util.Map;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.core.NestedRuntimeException;
import org.springframework.http.HttpStatus;
@ -72,12 +75,21 @@ public class ResponseStatusException extends NestedRuntimeException { @@ -72,12 +75,21 @@ public class ResponseStatusException extends NestedRuntimeException {
/**
* The HTTP status that fits the exception (never {@code null}).
* Return the HTTP status associated with this exception.
*/
public HttpStatus getStatus() {
return this.status;
}
/**
* Return response headers associated with the exception, possibly required
* for the given status code (e.g. "Allow", "Accept").
* @since 5.1.11
*/
public Map<String, String> getHeaders() {
return Collections.emptyMap();
}
/**
* The reason explaining the exception (potentially {@code null} or empty).
*/
@ -86,6 +98,7 @@ public class ResponseStatusException extends NestedRuntimeException { @@ -86,6 +98,7 @@ public class ResponseStatusException extends NestedRuntimeException {
return this.reason;
}
@Override
public String getMessage() {
String msg = this.status + (this.reason != null ? " \"" + this.reason + "\"" : "");

25
spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@ -22,6 +22,7 @@ import reactor.core.publisher.Mono; @@ -22,6 +22,7 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
@ -62,8 +63,7 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler { @@ -62,8 +63,7 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
HttpStatus status = resolveStatus(ex);
if (status == null || !exchange.getResponse().setStatusCode(status)) {
if (!updateResponse(exchange.getResponse(), ex)) {
return Mono.error(ex);
}
@ -86,16 +86,25 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler { @@ -86,16 +86,25 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler {
return "Resolved [" + reason + "] for HTTP " + request.getMethod() + " " + path;
}
@Nullable
private HttpStatus resolveStatus(Throwable ex) {
private boolean updateResponse(ServerHttpResponse response, Throwable ex) {
boolean result = false;
HttpStatus status = determineStatus(ex);
if (status == null) {
if (status != null) {
if (response.setStatusCode(status)) {
if (ex instanceof ResponseStatusException) {
((ResponseStatusException) ex).getHeaders()
.forEach((name, value) -> response.getHeaders().add(name, value));
}
result = true;
}
}
else {
Throwable cause = ex.getCause();
if (cause != null) {
status = resolveStatus(cause);
result = updateResponse(response, cause);
}
}
return status;
return result;
}
/**

26
spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java

@ -17,15 +17,21 @@ @@ -17,15 +17,21 @@
package org.springframework.web.server.handler;
import java.time.Duration;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.mock.web.test.server.MockServerWebExchange;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ResponseStatusException;
import static org.assertj.core.api.Assertions.assertThat;
@ -67,6 +73,26 @@ public class ResponseStatusExceptionHandlerTests { @@ -67,6 +73,26 @@ public class ResponseStatusExceptionHandlerTests {
assertThat(this.exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test // gh-23741
public void handleMethodNotAllowed() {
Throwable ex = new MethodNotAllowedException(HttpMethod.PATCH, Arrays.asList(HttpMethod.POST, HttpMethod.PUT));
this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5));
MockServerHttpResponse response = this.exchange.getResponse();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
assertThat(response.getHeaders().getAllow()).containsOnly(HttpMethod.POST, HttpMethod.PUT);
}
@Test // gh-23741
public void handleResponseStatusExceptionWithHeaders() {
Throwable ex = new NotAcceptableStatusException(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.TEXT_HTML));
this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5));
MockServerHttpResponse response = this.exchange.getResponse();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_ACCEPTABLE);
assertThat(response.getHeaders().getAccept()).containsOnly(MediaType.TEXT_PLAIN, MediaType.TEXT_HTML);
}
@Test
public void unresolvedException() {
Throwable expected = new IllegalStateException();

Loading…
Cancel
Save