Skip to content

Commit c5ef722

Browse files
authored
feat: Add Response factories for common statuses (#127)
Adds ok(), noContent(), notModified(), badRequest(), unauthorized(), forbidden(), conflict(), internalServerError(), methodNotAllowed(...) and JSON-body variants for 400/403/409/422, so handlers no longer need HttpURLConnection constants. Handlers and Cors use them.
1 parent 72333f9 commit c5ef722

5 files changed

Lines changed: 208 additions & 29 deletions

File tree

‎README.md‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,27 @@ public class PostDataHandler implements RequestHandler {
108108
`Response` is an immutable record built via static factories. Pick the one that fits:
109109

110110
``` java
111-
Response.empty(); // 204 No Content, no body
112-
Response.status(200); // 200 OK, no body
111+
Response.ok(); // 200 OK, no body
113112
Response.ok(Map.of("id", "42")); // 200 OK, JSON body via TypeMapper
114113
Response.created(newResource); // 201 Created, JSON body
115114
Response.created(newResource)
116115
.withHeader("Location", "/things/42"); // 201 Created + Location header
117116
Response.accepted(); // 202 Accepted, no body
118117
Response.accepted(Map.of("jobId", "job-42")); // 202 Accepted, JSON body
118+
Response.noContent(); // 204 No Content (alias: empty())
119+
Response.notModified(); // 304 Not Modified
120+
Response.badRequest(problemDetail); // 400 Bad Request, JSON body
121+
Response.unauthorized(); // 401 Unauthorized
122+
Response.forbidden(); // 403 Forbidden
119123
Response.notFound(); // 404 Not Found, no body
120124
Response.notFound(problemDetail); // 404 Not Found, JSON body
125+
Response.methodNotAllowed(GET, HEAD); // 405 + Allow: GET, HEAD
126+
Response.conflict(conflictDetail); // 409 Conflict, JSON body
127+
Response.unprocessableContent(problemDetail); // 422 Unprocessable Content, JSON body
128+
Response.internalServerError(); // 500 Internal Server Error
121129
Response.notImplemented(); // 501 Not Implemented, no body
122-
Response.of(409, conflictDetail); // any status, JSON body
130+
Response.status(418); // any status, no body
131+
Response.of(418, teapot); // any status, JSON body
123132
Response.text(200, "hello"); // text/plain; UTF-8
124133
Response.bytes(200, pdf, "application/pdf"); // pre-serialised bytes
125134
Response.stream(200, "application/octet-stream", // chunked streaming

‎src/main/java/com/retailsvc/http/Cors.java‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package com.retailsvc.http;
22

33
import static com.retailsvc.http.spec.HttpMethod.OPTIONS;
4-
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
5-
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
6-
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
74

85
import com.retailsvc.http.spec.HttpMethod;
96
import java.time.Duration;
@@ -21,8 +18,6 @@
2118
*/
2219
public final class Cors {
2320

24-
private static final String ALLOW = "Allow";
25-
2621
private Cors() {}
2722

2823
/**
@@ -94,13 +89,13 @@ public static RequestHandler preflightHandler(
9489

9590
return req -> {
9691
if (req.method() != OPTIONS) {
97-
return Response.status(HTTP_BAD_METHOD).withHeader(ALLOW, "OPTIONS");
92+
return Response.methodNotAllowed(OPTIONS);
9893
}
9994
String origin = requireHeader(req, "Origin");
10095
String requestMethod = requireHeader(req, "Access-Control-Request-Method");
10196
if (!isPreflightAllowed(
10297
req, origin, requestMethod, originAllowed, allowedMethods, headerAllowlistLower)) {
103-
return Response.status(HTTP_FORBIDDEN);
98+
return Response.forbidden();
10499
}
105100
return buildPreflightSuccess(
106101
origin,
@@ -166,7 +161,7 @@ private static Response buildPreflightSuccess(
166161
boolean allowCredentials,
167162
String maxAgeHeader) {
168163
Response resp =
169-
Response.status(HTTP_NO_CONTENT)
164+
Response.noContent()
170165
.withHeader("Access-Control-Allow-Origin", origin)
171166
.withHeader("Access-Control-Allow-Methods", allowMethodsHeader)
172167
.withHeader("Vary", "Origin");

‎src/main/java/com/retailsvc/http/Handlers.java‎

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
import static com.retailsvc.http.spec.HttpMethod.GET;
44
import static com.retailsvc.http.spec.HttpMethod.HEAD;
5-
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
65
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
7-
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
86
import static java.net.HttpURLConnection.HTTP_OK;
97
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
108
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -18,15 +16,12 @@
1816
import java.util.List;
1917
import java.util.Objects;
2018
import java.util.function.Supplier;
21-
import java.util.stream.Collectors;
2219
import org.slf4j.Logger;
2320
import org.slf4j.LoggerFactory;
2421

2522
public final class Handlers {
2623

2724
private static final Logger LOG = LoggerFactory.getLogger(Handlers.class);
28-
private static final String ALLOW = "Allow";
29-
private static final String GET_HEAD = "GET, HEAD";
3025

3126
private Handlers() {}
3227

@@ -79,14 +74,10 @@ public static ExceptionHandler defaultExceptionHandler() {
7974
}
8075
yield Response.notFound();
8176
}
82-
case MethodNotAllowedException mna ->
83-
Response.status(HTTP_BAD_METHOD)
84-
.withHeader(
85-
ALLOW,
86-
mna.allowed().stream().map(Enum::name).collect(Collectors.joining(", ")));
77+
case MethodNotAllowedException mna -> Response.methodNotAllowed(mna.allowed());
8778
default -> {
8879
LOG.error("Unhandled exception in handler", t);
89-
yield Response.status(HTTP_INTERNAL_ERROR);
80+
yield Response.internalServerError();
9081
}
9182
};
9283
}
@@ -96,7 +87,7 @@ public static RequestHandler aliveHandler() {
9687
return req ->
9788
switch (req.method()) {
9889
case GET, HEAD -> Response.empty();
99-
default -> Response.status(HTTP_BAD_METHOD).withHeader(ALLOW, GET_HEAD);
90+
default -> Response.methodNotAllowed(GET, HEAD);
10091
};
10192
}
10293

@@ -121,7 +112,7 @@ public static RequestHandler healthHandler(Supplier<HealthOutcome> probe) {
121112
Objects.requireNonNull(probe, "probe");
122113
return req -> {
123114
if (req.method() != GET && req.method() != HEAD) {
124-
return Response.status(HTTP_BAD_METHOD).withHeader(ALLOW, GET_HEAD);
115+
return Response.methodNotAllowed(GET, HEAD);
125116
}
126117
boolean up;
127118
List<Dependency> dependencies;
@@ -177,10 +168,10 @@ private static RequestHandler resourceHandler(ResourceSource source) {
177168
}
178169
});
179170
case HEAD ->
180-
Response.status(HTTP_OK)
171+
Response.ok()
181172
.withContentType(contentType)
182173
.withHeader("Content-Length", String.valueOf(length));
183-
default -> Response.status(HTTP_BAD_METHOD).withHeader(ALLOW, GET_HEAD);
174+
default -> Response.methodNotAllowed(GET, HEAD);
184175
};
185176
}
186177
}

‎src/main/java/com/retailsvc/http/Response.java‎

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
package com.retailsvc.http;
22

33
import static java.net.HttpURLConnection.HTTP_ACCEPTED;
4+
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
5+
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
6+
import static java.net.HttpURLConnection.HTTP_CONFLICT;
47
import static java.net.HttpURLConnection.HTTP_CREATED;
8+
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
9+
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
510
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
611
import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
12+
import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
713
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
814
import static java.net.HttpURLConnection.HTTP_OK;
15+
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
916

1017
import com.retailsvc.http.internal.BodyWriter;
18+
import com.retailsvc.http.spec.HttpMethod;
1119
import java.io.IOException;
1220
import java.io.OutputStream;
1321
import java.nio.charset.StandardCharsets;
22+
import java.util.Collection;
1423
import java.util.LinkedHashMap;
24+
import java.util.List;
1525
import java.util.Map;
26+
import java.util.stream.Collectors;
1627

1728
/**
1829
* The value returned by every {@link RequestHandler}. Carries status, optional body, optional
@@ -32,22 +43,83 @@
3243
*/
3344
public record Response(int status, Object body, String contentType, Map<String, String> headers) {
3445

46+
/** Not defined by {@link java.net.HttpURLConnection}. */
47+
private static final int HTTP_UNPROCESSABLE_CONTENT = 422;
48+
3549
public Response {
3650
headers = headers == null ? Map.of() : Map.copyOf(headers);
3751
}
3852

3953
// -- one-shot, no-body --
4054

41-
/** {@code 204 No Content} with no body. */
55+
/** {@code 204 No Content} with no body. Same as {@link #noContent()}. */
4256
public static Response empty() {
43-
return new Response(HTTP_NO_CONTENT, null, null, Map.of());
57+
return noContent();
4458
}
4559

46-
/** Given status, no body. Use for {@code 200 OK} no body, {@code 404}, {@code 405}, etc. */
60+
/** Given status, no body. Prefer a named factory such as {@link #ok()} when one exists. */
4761
public static Response status(int status) {
4862
return new Response(status, null, null, Map.of());
4963
}
5064

65+
/** {@code 200 OK} with no body. */
66+
public static Response ok() {
67+
return status(HTTP_OK);
68+
}
69+
70+
/** {@code 204 No Content} with no body. */
71+
public static Response noContent() {
72+
return status(HTTP_NO_CONTENT);
73+
}
74+
75+
/** {@code 304 Not Modified} with no body. */
76+
public static Response notModified() {
77+
return status(HTTP_NOT_MODIFIED);
78+
}
79+
80+
/** {@code 400 Bad Request} with no body. */
81+
public static Response badRequest() {
82+
return status(HTTP_BAD_REQUEST);
83+
}
84+
85+
/** {@code 401 Unauthorized} with no body. Add a {@code WWW-Authenticate} header as needed. */
86+
public static Response unauthorized() {
87+
return status(HTTP_UNAUTHORIZED);
88+
}
89+
90+
/** {@code 403 Forbidden} with no body. */
91+
public static Response forbidden() {
92+
return status(HTTP_FORBIDDEN);
93+
}
94+
95+
/**
96+
* {@code 405 Method Not Allowed} with no body and an {@code Allow} header listing {@code
97+
* allowed}.
98+
*/
99+
public static Response methodNotAllowed(HttpMethod... allowed) {
100+
return methodNotAllowed(List.of(allowed));
101+
}
102+
103+
/**
104+
* {@code 405 Method Not Allowed} with no body and an {@code Allow} header listing {@code allowed}
105+
* in {@link HttpMethod} declaration order.
106+
*/
107+
public static Response methodNotAllowed(Collection<HttpMethod> allowed) {
108+
String allow =
109+
allowed.stream().sorted().distinct().map(Enum::name).collect(Collectors.joining(", "));
110+
return status(HTTP_BAD_METHOD).withHeader("Allow", allow);
111+
}
112+
113+
/** {@code 409 Conflict} with no body. */
114+
public static Response conflict() {
115+
return status(HTTP_CONFLICT);
116+
}
117+
118+
/** {@code 500 Internal Server Error} with no body. */
119+
public static Response internalServerError() {
120+
return status(HTTP_INTERNAL_ERROR);
121+
}
122+
51123
// -- one-shot, JSON body --
52124

53125
/** {@code 200 OK} with {@code body} serialised as JSON. */
@@ -73,6 +145,16 @@ public static Response accepted(Object body) {
73145
return new Response(HTTP_ACCEPTED, body, null, Map.of());
74146
}
75147

148+
/** {@code 400 Bad Request} with {@code body} serialised as JSON (e.g. a ProblemDetail). */
149+
public static Response badRequest(Object body) {
150+
return new Response(HTTP_BAD_REQUEST, body, null, Map.of());
151+
}
152+
153+
/** {@code 403 Forbidden} with {@code body} serialised as JSON (e.g. a ProblemDetail). */
154+
public static Response forbidden(Object body) {
155+
return new Response(HTTP_FORBIDDEN, body, null, Map.of());
156+
}
157+
76158
/** {@code 404 Not Found} with no body. */
77159
public static Response notFound() {
78160
return new Response(HTTP_NOT_FOUND, null, null, Map.of());
@@ -83,6 +165,19 @@ public static Response notFound(Object body) {
83165
return new Response(HTTP_NOT_FOUND, body, null, Map.of());
84166
}
85167

168+
/** {@code 409 Conflict} with {@code body} serialised as JSON (e.g. a ProblemDetail). */
169+
public static Response conflict(Object body) {
170+
return new Response(HTTP_CONFLICT, body, null, Map.of());
171+
}
172+
173+
/**
174+
* {@code 422 Unprocessable Content} with {@code body} serialised as JSON (e.g. a ProblemDetail).
175+
* Use when the request is well-formed but breaks a business rule.
176+
*/
177+
public static Response unprocessableContent(Object body) {
178+
return new Response(HTTP_UNPROCESSABLE_CONTENT, body, null, Map.of());
179+
}
180+
86181
/** {@code 501 Not Implemented} with no body. */
87182
public static Response notImplemented() {
88183
return new Response(HTTP_NOT_IMPLEMENTED, null, null, Map.of());

0 commit comments

Comments
 (0)