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/27217/head
Rossen Stoyanchev 6 years ago
parent
commit
614c7b0f8e
  1. 23
      spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java
  2. 13
      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. 33
      spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java

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

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,12 +19,15 @@ package org.springframework.web.server;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/** /**
* Exception for errors that fit response status 405 (method not allowed). * Exception for errors that fit response status 405 (method not allowed).
@ -37,7 +40,7 @@ public class MethodNotAllowedException extends ResponseStatusException {
private final String method; private final String method;
private final Set<HttpMethod> supportedMethods; private final Set<HttpMethod> httpMethods;
public MethodNotAllowedException(HttpMethod method, Collection<HttpMethod> supportedMethods) { public MethodNotAllowedException(HttpMethod method, Collection<HttpMethod> supportedMethods) {
@ -51,10 +54,21 @@ public class MethodNotAllowedException extends ResponseStatusException {
supportedMethods = Collections.emptySet(); supportedMethods = Collections.emptySet();
} }
this.method = method; this.method = method;
this.supportedMethods = Collections.unmodifiableSet(new HashSet<>(supportedMethods)); this.httpMethods = Collections.unmodifiableSet(new HashSet<>(supportedMethods));
} }
/**
* Return a Map with an "Allow" header.
* @since 5.1.11
*/
@Override
public Map<String, String> getHeaders() {
return !CollectionUtils.isEmpty(this.httpMethods) ?
Collections.singletonMap("Allow", StringUtils.collectionToDelimitedString(this.httpMethods, ", ")) :
Collections.emptyMap();
}
/** /**
* Return the HTTP method for the failed request. * Return the HTTP method for the failed request.
*/ */
@ -66,6 +80,7 @@ public class MethodNotAllowedException extends ResponseStatusException {
* Return the list of supported HTTP methods. * Return the list of supported HTTP methods.
*/ */
public Set<HttpMethod> getSupportedMethods() { public Set<HttpMethod> getSupportedMethods() {
return this.supportedMethods; return this.httpMethods;
} }
} }

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

@ -18,9 +18,11 @@ package org.springframework.web.server;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
/** /**
* Exception for errors that fit response status 406 (not acceptable). * Exception for errors that fit response status 406 (not acceptable).
@ -51,6 +53,17 @@ public class NotAcceptableStatusException extends ResponseStatusException {
} }
/**
* Return a Map with an "Accept" header, or an empty map.
* @since 5.1.11
*/
@Override
public Map<String, String> getHeaders() {
return !CollectionUtils.isEmpty(this.supportedMediaTypes) ?
Collections.singletonMap("Accept", MediaType.toString(this.supportedMediaTypes)) :
Collections.emptyMap();
}
/** /**
* Return the list of supported content types in cases when the Accept * Return the list of supported content types in cases when the Accept
* header is parsed but not supported, or an empty list otherwise. * 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 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,9 @@
package org.springframework.web.server; package org.springframework.web.server;
import java.util.Collections;
import java.util.Map;
import org.springframework.core.NestedExceptionUtils; import org.springframework.core.NestedExceptionUtils;
import org.springframework.core.NestedRuntimeException; import org.springframework.core.NestedRuntimeException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -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() { public HttpStatus getStatus() {
return this.status; 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). * The reason explaining the exception (potentially {@code null} or empty).
*/ */
@ -86,6 +98,7 @@ public class ResponseStatusException extends NestedRuntimeException {
return this.reason; return this.reason;
} }
@Override @Override
public String getMessage() { public String getMessage() {
String msg = this.status + (this.reason != null ? " \"" + this.reason + "\"" : ""); 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 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
@ -62,8 +63,7 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler {
@Override @Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
HttpStatus status = resolveStatus(ex); if (!updateResponse(exchange.getResponse(), ex)) {
if (status == null || !exchange.getResponse().setStatusCode(status)) {
return Mono.error(ex); return Mono.error(ex);
} }
@ -86,16 +86,25 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler {
return "Resolved [" + reason + "] for HTTP " + request.getMethod() + " " + path; return "Resolved [" + reason + "] for HTTP " + request.getMethod() + " " + path;
} }
@Nullable private boolean updateResponse(ServerHttpResponse response, Throwable ex) {
private HttpStatus resolveStatus(Throwable ex) { boolean result = false;
HttpStatus status = determineStatus(ex); 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(); Throwable cause = ex.getCause();
if (cause != null) { if (cause != null) {
status = resolveStatus(cause); result = updateResponse(response, cause);
} }
} }
return status; return result;
} }
/** /**

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

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,18 +17,27 @@
package org.springframework.web.server.handler; package org.springframework.web.server.handler;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; 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.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.mock.web.test.server.MockServerWebExchange; 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 org.springframework.web.server.ResponseStatusException;
import static org.junit.Assert.*; import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
/** /**
* Unit tests for {@link ResponseStatusExceptionHandler}. * Unit tests for {@link ResponseStatusExceptionHandler}.
@ -67,6 +76,26 @@ public class ResponseStatusExceptionHandlerTests {
assertEquals(HttpStatus.BAD_REQUEST, this.exchange.getResponse().getStatusCode()); assertEquals(HttpStatus.BAD_REQUEST, this.exchange.getResponse().getStatusCode());
} }
@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();
assertEquals(HttpStatus.METHOD_NOT_ALLOWED, response.getStatusCode());
assertThat(response.getHeaders().getAllow(), contains(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();
assertEquals(HttpStatus.NOT_ACCEPTABLE, response.getStatusCode());
assertThat(response.getHeaders().getAccept(), contains(MediaType.TEXT_PLAIN, MediaType.TEXT_HTML));
}
@Test @Test
public void unresolvedException() { public void unresolvedException() {
Throwable expected = new IllegalStateException(); Throwable expected = new IllegalStateException();

Loading…
Cancel
Save