Improve API for RFC 7807 in functional endpoints
Closes gh-29462
This commit is contained in:
@@ -263,13 +263,13 @@ public class ResponseEntity<T> extends HttpEntity<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder for a {@code ResponseEntity} with the given
|
||||
* {@link ProblemDetail} as the body, and its
|
||||
* {@link ProblemDetail#getStatus() status} as the status.
|
||||
* <p>Note that {@code ProblemDetail} is supported as a return value from
|
||||
* controller methods and from {@code @ExceptionHandler} methods. The method
|
||||
* here is convenient to also add response headers.
|
||||
* @param body the details for an HTTP error response
|
||||
* Create a new {@link HeadersBuilder} with its status set to
|
||||
* {@link ProblemDetail#getStatus()} and its body is set to
|
||||
* {@link ProblemDetail}.
|
||||
* <p><strong>Note:</strong> If there are no headers to add, there is usually
|
||||
* no need to create a {@link ResponseEntity} since {@code ProblemDetail}
|
||||
* is also supported as a return value from controller methods.
|
||||
* @param body the problem detail to use
|
||||
* @return the created builder
|
||||
* @since 6.0
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2002-2022 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.
|
||||
* 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 org.springframework.web;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ErrorResponse.Builder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.0
|
||||
*/
|
||||
final class DefaultErrorResponseBuilder implements ErrorResponse.Builder {
|
||||
|
||||
private final Throwable exception;
|
||||
|
||||
private final HttpStatusCode statusCode;
|
||||
|
||||
@Nullable
|
||||
private HttpHeaders headers;
|
||||
|
||||
private final ProblemDetail problemDetail;
|
||||
|
||||
private String detailMessageCode;
|
||||
|
||||
@Nullable
|
||||
private Object[] detailMessageArguments;
|
||||
|
||||
private String titleMessageCode;
|
||||
|
||||
|
||||
DefaultErrorResponseBuilder(Throwable ex, HttpStatusCode statusCode, String detail) {
|
||||
Assert.notNull(ex, "Throwable is required");
|
||||
Assert.notNull(ex, "HttpStatusCode is required");
|
||||
Assert.notNull(ex, "`detail` is required");
|
||||
this.exception = ex;
|
||||
this.statusCode = statusCode;
|
||||
this.problemDetail = ProblemDetail.forStatusAndDetail(statusCode, detail);
|
||||
this.detailMessageCode = ErrorResponse.getDefaultDetailMessageCode(ex.getClass(), null);
|
||||
this.titleMessageCode = ErrorResponse.getDefaultTitleMessageCode(ex.getClass());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder header(String headerName, String... headerValues) {
|
||||
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
|
||||
for (String headerValue : headerValues) {
|
||||
this.headers.add(headerName, headerValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder detail(String detail) {
|
||||
this.problemDetail.setDetail(detail);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder detailMessageCode(String messageCode) {
|
||||
Assert.notNull(messageCode, "`detailMessageCode` is required");
|
||||
this.detailMessageCode = messageCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder detailMessageArguments(Object... messageArguments) {
|
||||
this.detailMessageArguments = messageArguments;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder type(URI type) {
|
||||
this.problemDetail.setType(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder title(@Nullable String title) {
|
||||
this.problemDetail.setTitle(title);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder titleMessageCode(String messageCode) {
|
||||
Assert.notNull(messageCode, "`titleMessageCode` is required");
|
||||
this.titleMessageCode = messageCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder instance(@Nullable URI instance) {
|
||||
this.problemDetail.setInstance(instance);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse.Builder property(String name, Object value) {
|
||||
this.problemDetail.setProperty(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponse build() {
|
||||
return new SimpleErrorResponse(
|
||||
this.exception, this.statusCode, this.headers, this.problemDetail,
|
||||
this.detailMessageCode, this.detailMessageArguments, this.titleMessageCode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Simple container for {@code ErrorResponse} values.
|
||||
*/
|
||||
private static class SimpleErrorResponse implements ErrorResponse {
|
||||
|
||||
private final Throwable exception;
|
||||
|
||||
private final HttpStatusCode statusCode;
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private final ProblemDetail problemDetail;
|
||||
|
||||
private final String detailMessageCode;
|
||||
|
||||
@Nullable
|
||||
private final Object[] detailMessageArguments;
|
||||
|
||||
private final String titleMessageCode;
|
||||
|
||||
SimpleErrorResponse(
|
||||
Throwable ex, HttpStatusCode statusCode, @Nullable HttpHeaders headers, ProblemDetail problemDetail,
|
||||
String detailMessageCode, @Nullable Object[] detailMessageArguments, String titleMessageCode) {
|
||||
|
||||
this.exception = ex;
|
||||
this.statusCode = statusCode;
|
||||
this.headers = (headers != null ? headers : HttpHeaders.EMPTY);
|
||||
this.problemDetail = problemDetail;
|
||||
this.detailMessageCode = detailMessageCode;
|
||||
this.detailMessageArguments = detailMessageArguments;
|
||||
this.titleMessageCode = titleMessageCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatusCode getStatusCode() {
|
||||
return this.statusCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProblemDetail getBody() {
|
||||
return this.problemDetail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDetailMessageCode() {
|
||||
return this.detailMessageCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getDetailMessageArguments() {
|
||||
return this.detailMessageArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitleMessageCode() {
|
||||
return this.titleMessageCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ErrorResponse{status=" + this.statusCode + ", " +
|
||||
"headers=" + this.headers + ", body=" + this.problemDetail + ", " +
|
||||
"exception=" + this.exception + "}";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.web;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -148,35 +150,124 @@ public interface ErrorResponse {
|
||||
return "problemDetail.title." + exceptionType.getName();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Map the given Exception to an {@link ErrorResponse}.
|
||||
* @param ex the Exception, mostly to derive message codes, if not provided
|
||||
* @param status the response status to use
|
||||
* @param headers optional headers to add to the response
|
||||
* @param defaultDetail default value for the "detail" field
|
||||
* @param detailMessageCode the code to use to look up the "detail" field
|
||||
* through a {@code MessageSource}, falling back on
|
||||
* {@link #getDefaultDetailMessageCode(Class, String)}
|
||||
* @param detailMessageArguments the arguments to go with the detailMessageCode
|
||||
* @return the created {@code ErrorResponse} instance
|
||||
* Static factory method to build an instance via
|
||||
* {@link #builder(Throwable, HttpStatusCode, String)}.
|
||||
*/
|
||||
static ErrorResponse createFor(
|
||||
Exception ex, HttpStatusCode status, @Nullable HttpHeaders headers,
|
||||
String defaultDetail, @Nullable String detailMessageCode, @Nullable Object[] detailMessageArguments) {
|
||||
static ErrorResponse create(Throwable ex, HttpStatusCode statusCode, String detail) {
|
||||
return builder(ex, statusCode, detail).build();
|
||||
}
|
||||
|
||||
if (detailMessageCode == null) {
|
||||
detailMessageCode = ErrorResponse.getDefaultDetailMessageCode(ex.getClass(), null);
|
||||
}
|
||||
/**
|
||||
* Return a builder to create an {@code ErrorResponse} instance.
|
||||
* @param ex the underlying exception that lead to the error response;
|
||||
* mainly to derive default values for the
|
||||
* {@link #getDetailMessageCode() detail message code} and for the
|
||||
* {@link #getTitleMessageCode() title message code}.
|
||||
* @param statusCode the status code to set the response to
|
||||
* @param detail the default value for the
|
||||
* {@link ProblemDetail#setDetail(String) detail} field, unless overridden
|
||||
* by a {@link MessageSource} lookup with {@link #getDetailMessageCode()}
|
||||
*/
|
||||
static Builder builder(Throwable ex, HttpStatusCode statusCode, String detail) {
|
||||
return new DefaultErrorResponseBuilder(ex, statusCode, detail);
|
||||
}
|
||||
|
||||
ErrorResponseException errorResponse = new ErrorResponseException(
|
||||
status, ProblemDetail.forStatusAndDetail(status, defaultDetail), null,
|
||||
detailMessageCode, detailMessageArguments);
|
||||
|
||||
if (headers != null) {
|
||||
errorResponse.getHeaders().putAll(headers);
|
||||
}
|
||||
/**
|
||||
* Builder for an {@code ErrorResponse}.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Add the given header value(s) under the given name.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header value(s)
|
||||
* @return the same builder instance
|
||||
* @see HttpHeaders#add(String, String)
|
||||
*/
|
||||
Builder header(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Manipulate this response's headers with the given consumer. This is
|
||||
* useful to {@linkplain HttpHeaders#set(String, String) overwrite} or
|
||||
* {@linkplain HttpHeaders#remove(Object) remove} existing values, or
|
||||
* use any other {@link HttpHeaders} methods.
|
||||
* @param headersConsumer a function that consumes the {@code HttpHeaders}
|
||||
* @return the same builder instance
|
||||
*/
|
||||
Builder headers(Consumer<HttpHeaders> headersConsumer);
|
||||
|
||||
/**
|
||||
* Set the underlying {@link ProblemDetail#setDetail(String)}.
|
||||
* @return the same builder instance
|
||||
*/
|
||||
Builder detail(String detail);
|
||||
|
||||
/**
|
||||
* Customize the {@link MessageSource} code for looking up the value for
|
||||
* the underlying {@link #detail(String)}.
|
||||
* <p>By default, this is set to
|
||||
* {@link ErrorResponse#getDefaultDetailMessageCode(Class, String)} with the
|
||||
* associated Exception type.
|
||||
* @param messageCode the message code to use
|
||||
* @return the same builder instance
|
||||
* @see ErrorResponse#getDetailMessageCode()
|
||||
*/
|
||||
Builder detailMessageCode(String messageCode);
|
||||
|
||||
/**
|
||||
* Set the arguments to provide to the {@link MessageSource} lookup for
|
||||
* {@link #detailMessageCode(String)}.
|
||||
* @param messageArguments the arguments to provide
|
||||
* @return the same builder instance
|
||||
* @see ErrorResponse#getDetailMessageArguments()
|
||||
*/
|
||||
Builder detailMessageArguments(Object... messageArguments);
|
||||
|
||||
/**
|
||||
* Set the underlying {@link ProblemDetail#setTitle(String)} field.
|
||||
* @return the same builder instance
|
||||
*/
|
||||
Builder type(URI type);
|
||||
|
||||
/**
|
||||
* Set the underlying {@link ProblemDetail#setTitle(String)} field.
|
||||
* @return the same builder instance
|
||||
*/
|
||||
Builder title(@Nullable String title);
|
||||
|
||||
/**
|
||||
* Customize the {@link MessageSource} code for looking up the value for
|
||||
* the underlying {@link ProblemDetail#setTitle(String)}.
|
||||
* <p>By default, set via
|
||||
* {@link ErrorResponse#getDefaultTitleMessageCode(Class)} with the
|
||||
* associated Exception type.
|
||||
* @param messageCode the message code to use
|
||||
* @return the same builder instance
|
||||
* @see ErrorResponse#getTitleMessageCode()
|
||||
*/
|
||||
Builder titleMessageCode(String messageCode);
|
||||
|
||||
/**
|
||||
* Set the underlying {@link ProblemDetail#setInstance(URI)} field.
|
||||
* @return the same builder instance
|
||||
*/
|
||||
Builder instance(@Nullable URI instance);
|
||||
|
||||
/**
|
||||
* Set a "dynamic" {@link ProblemDetail#setProperty(String, Object)
|
||||
* property} on the underlying {@code ProblemDetail}.
|
||||
* @return the same builder instance
|
||||
*/
|
||||
Builder property(String name, Object value);
|
||||
|
||||
/**
|
||||
* Build the {@code ErrorResponse} instance.
|
||||
*/
|
||||
ErrorResponse build();
|
||||
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user