Skip to content

Commit

Permalink
ResponseStatusException associated headers
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rstoyanchev committed Oct 30, 2019
1 parent cc84533 commit 614c7b0
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -19,12 +19,15 @@
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.CollectionUtils;
import org.springframework.util.StringUtils;

/**
* Exception for errors that fit response status 405 (method not allowed).
Expand All @@ -37,7 +40,7 @@ public class MethodNotAllowedException extends ResponseStatusException {

private final String method;

private final Set<HttpMethod> supportedMethods;
private final Set<HttpMethod> httpMethods;


public MethodNotAllowedException(HttpMethod method, Collection<HttpMethod> supportedMethods) {
Expand All @@ -51,10 +54,21 @@ public MethodNotAllowedException(String method, @Nullable Collection<HttpMethod>
supportedMethods = Collections.emptySet();
}
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.
*/
Expand All @@ -66,6 +80,7 @@ public String getHttpMethod() {
* Return the list of supported HTTP methods.
*/
public Set<HttpMethod> getSupportedMethods() {
return this.supportedMethods;
return this.httpMethods;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@

import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;

/**
* Exception for errors that fit response status 406 (not acceptable).
Expand Down Expand Up @@ -51,6 +53,17 @@ public NotAcceptableStatusException(List<MediaType> supportedMediaTypes) {
}


/**
* 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
* header is parsed but not supported, or an empty list otherwise.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -72,12 +75,21 @@ public ResponseStatusException(HttpStatus status, @Nullable String reason, @Null


/**
* 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).
*/
Expand All @@ -86,6 +98,7 @@ public String getReason() {
return this.reason;
}


@Override
public String getMessage() {
String msg = this.status + (this.reason != null ? " \"" + this.reason + "\"" : "");
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -22,6 +22,7 @@

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;
Expand Down Expand Up @@ -62,8 +63,7 @@ public void setWarnLogCategory(String loggerName) {

@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);
}

Expand All @@ -86,16 +86,25 @@ private String formatError(Throwable ex, ServerHttpRequest request) {
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()

This comment has been minimized.

Copy link
@KanyCTa

KanyCTa Dec 24, 2019

Hello everyone! Faced with problem: new mech cant transfer multivalues headers like "WWW-Authenticate: Negotiate, NTLM, etc" in raw-style (in a Map<String, List> in this case). ResponseStatusException.getHeaders returns simple Map. I can transfer multivalues only by converting to delimeted string and from it in exception handler. ServerHttpResponse.getHeaders() is HttpHeaders that implements MultiValueMap. Are any restrictions here for using MultiValueMap in ResponseStatusException.getHeaders() instead of Map? Thanks!

This comment has been minimized.

Copy link
@rstoyanchev

rstoyanchev Jan 2, 2020

Author Contributor

No particular reason. A MultiValueMap would have been better indeed.

.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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,18 +17,27 @@
package org.springframework.web.server.handler;

import java.time.Duration;
import java.util.Arrays;

import org.junit.Before;
import org.junit.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.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}.
Expand Down Expand Up @@ -67,6 +76,26 @@ public void handleNestedResponseStatusException() {
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
public void unresolvedException() {
Throwable expected = new IllegalStateException();
Expand Down

0 comments on commit 614c7b0

Please sign in to comment.