Skip to content

Commit

Permalink
Improve API for RFC 7807 in functional endpoints
Browse files Browse the repository at this point in the history
Closes gh-29462
  • Loading branch information
rstoyanchev committed Nov 11, 2022
1 parent 9d1dfc7 commit 0348a7b
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 38 deletions.
Expand Up @@ -263,13 +263,13 @@ public static <T> ResponseEntity<T> of(Optional<T> body) {
}

/**
* 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
*/
Expand Down
@@ -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");

This comment has been minimized.

Copy link
@ophiuhus

ophiuhus Nov 15, 2022

Contributor

Shouldn't it be
Assert.notNull(statusCode, "HttpStatusCode is required");
Assert.notNull(detail, "detail is required");
instead?

This comment has been minimized.

Copy link
@bclozel

bclozel Nov 15, 2022

Member

@ophiuhus Good catch! Would you like to send a PR for that?

This comment has been minimized.

Copy link
@ophiuhus

ophiuhus Nov 15, 2022

Contributor

Will do, thanks

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 + "}";
}
}

}
137 changes: 114 additions & 23 deletions spring-web/src/main/java/org/springframework/web/ErrorResponse.java
Expand Up @@ -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;
Expand Down Expand Up @@ -148,35 +150,124 @@ static String getDefaultTitleMessageCode(Class<?> exceptionType) {
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;
}

}

0 comments on commit 0348a7b

Please sign in to comment.