Skip to content

Commit 5dc72ca

Browse files
authored
Merge pull request #34 from DataDog/feat/return-structured-errors
feat(award-service): sticker-award to return structured errors according to spec
2 parents b458dd2 + ffefa93 commit 5dc72ca

8 files changed

Lines changed: 534 additions & 23 deletions

File tree

sticker-award/src/main/java/com/datadoghq/stickerlandia/stickeraward/award/StickerAwardResource.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.datadoghq.stickerlandia.stickeraward.award.dto.GetUserStickersResponse;
66
import com.datadoghq.stickerlandia.stickeraward.award.dto.RemoveStickerFromUserResponse;
77
import com.datadoghq.stickerlandia.stickeraward.award.messaging.StickerAwardEventPublisher;
8+
import com.datadoghq.stickerlandia.stickeraward.common.exception.ProblemDetailsResponseBuilder;
89
import io.smallrye.common.constraint.NotNull;
910
import jakarta.inject.Inject;
1011
import jakarta.transaction.Transactional;
@@ -58,7 +59,8 @@ public Response assignStickerToUser(
5859
AssignStickerResponse response =
5960
stickerAwardRepository.assignStickerToUser(userId, stickerId, request);
6061
if (response == null) {
61-
return Response.status(Response.Status.BAD_REQUEST).build();
62+
return ProblemDetailsResponseBuilder.badRequest(
63+
"Invalid request or sticker assignment failed");
6264
}
6365

6466
// Publish events - Note: We'll need to modify event publisher to work with DTOs
@@ -73,7 +75,8 @@ public Response assignStickerToUser(
7375

7476
return Response.status(Response.Status.CREATED).entity(response).build();
7577
} catch (IllegalStateException e) {
76-
return Response.status(Response.Status.CONFLICT).build();
78+
return ProblemDetailsResponseBuilder.conflict(
79+
"Sticker assignment conflict: " + e.getMessage());
7780
}
7881
}
7982

@@ -95,7 +98,11 @@ public Response removeStickerFromUser(
9598
RemoveStickerFromUserResponse response =
9699
stickerAwardRepository.removeStickerFromUser(userId, stickerId);
97100
if (response == null) {
98-
return Response.status(Response.Status.NOT_FOUND).build();
101+
return ProblemDetailsResponseBuilder.notFound(
102+
"Sticker assignment not found for user "
103+
+ userId
104+
+ " and sticker "
105+
+ stickerId);
99106
}
100107

101108
// Publish events - Note: We'll need to modify event publisher to work with DTOs
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.datadoghq.stickerlandia.stickeraward.common.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import java.util.Map;
6+
7+
/** RFC 7807 Problem Details for HTTP APIs. */
8+
@JsonInclude(JsonInclude.Include.NON_NULL)
9+
public class ProblemDetails {
10+
@JsonProperty("type")
11+
private String type;
12+
13+
@JsonProperty("title")
14+
private String title;
15+
16+
@JsonProperty("status")
17+
private Integer status;
18+
19+
@JsonProperty("detail")
20+
private String detail;
21+
22+
@JsonProperty("instance")
23+
private String instance;
24+
25+
private Map<String, Object> additionalProperties;
26+
27+
/** Default constructor. */
28+
public ProblemDetails() {}
29+
30+
/**
31+
* Constructor with status and title.
32+
*
33+
* @param status HTTP status code
34+
* @param title brief, human-readable message
35+
*/
36+
public ProblemDetails(int status, String title) {
37+
this.status = status;
38+
this.title = title;
39+
}
40+
41+
/**
42+
* Constructor with status, title, and detail.
43+
*
44+
* @param status HTTP status code
45+
* @param title brief, human-readable message
46+
* @param detail human-readable explanation
47+
*/
48+
public ProblemDetails(int status, String title, String detail) {
49+
this.status = status;
50+
this.title = title;
51+
this.detail = detail;
52+
}
53+
54+
/**
55+
* Constructor with type, status, title, and detail.
56+
*
57+
* @param type URI reference that identifies the problem type
58+
* @param status HTTP status code
59+
* @param title brief, human-readable message
60+
* @param detail human-readable explanation
61+
*/
62+
public ProblemDetails(String type, int status, String title, String detail) {
63+
this.type = type;
64+
this.status = status;
65+
this.title = title;
66+
this.detail = detail;
67+
}
68+
69+
/**
70+
* Full constructor with all RFC 7807 fields.
71+
*
72+
* @param type URI reference that identifies the problem type
73+
* @param status HTTP status code
74+
* @param title brief, human-readable message
75+
* @param detail human-readable explanation
76+
* @param instance URI reference that identifies the specific occurrence
77+
*/
78+
public ProblemDetails(String type, int status, String title, String detail, String instance) {
79+
this.type = type;
80+
this.status = status;
81+
this.title = title;
82+
this.detail = detail;
83+
this.instance = instance;
84+
}
85+
86+
public String getType() {
87+
return type;
88+
}
89+
90+
public void setType(String type) {
91+
this.type = type;
92+
}
93+
94+
public String getTitle() {
95+
return title;
96+
}
97+
98+
public void setTitle(String title) {
99+
this.title = title;
100+
}
101+
102+
public Integer getStatus() {
103+
return status;
104+
}
105+
106+
public void setStatus(Integer status) {
107+
this.status = status;
108+
}
109+
110+
public String getDetail() {
111+
return detail;
112+
}
113+
114+
public void setDetail(String detail) {
115+
this.detail = detail;
116+
}
117+
118+
public String getInstance() {
119+
return instance;
120+
}
121+
122+
public void setInstance(String instance) {
123+
this.instance = instance;
124+
}
125+
126+
public Map<String, Object> getAdditionalProperties() {
127+
return additionalProperties;
128+
}
129+
130+
public void setAdditionalProperties(Map<String, Object> additionalProperties) {
131+
this.additionalProperties = additionalProperties;
132+
}
133+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.datadoghq.stickerlandia.stickeraward.common.exception;
2+
3+
import com.datadoghq.stickerlandia.stickeraward.common.dto.ProblemDetails;
4+
import jakarta.ws.rs.WebApplicationException;
5+
import jakarta.ws.rs.core.Response;
6+
import jakarta.ws.rs.ext.ExceptionMapper;
7+
import jakarta.ws.rs.ext.Provider;
8+
9+
/** JAX-RS exception mapper for WebApplicationException to RFC 7807 ProblemDetails. */
10+
@Provider
11+
public class ProblemDetailsExceptionMapper implements ExceptionMapper<WebApplicationException> {
12+
13+
private static final String PROBLEM_JSON_TYPE = "application/problem+json";
14+
15+
@Override
16+
public Response toResponse(WebApplicationException exception) {
17+
int status = exception.getResponse().getStatus();
18+
String title = getDefaultTitle(status);
19+
String detail = exception.getMessage();
20+
21+
ProblemDetails problemDetails = new ProblemDetails(status, title, detail);
22+
23+
return Response.status(status)
24+
.entity(problemDetails)
25+
.header("Content-Type", PROBLEM_JSON_TYPE)
26+
.build();
27+
}
28+
29+
private String getDefaultTitle(int status) {
30+
return switch (status) {
31+
case 400 -> "Bad Request";
32+
case 401 -> "Unauthorized";
33+
case 403 -> "Forbidden";
34+
case 404 -> "Not Found";
35+
case 409 -> "Conflict";
36+
case 500 -> "Internal Server Error";
37+
default -> "Error";
38+
};
39+
}
40+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.datadoghq.stickerlandia.stickeraward.common.exception;
2+
3+
import com.datadoghq.stickerlandia.stickeraward.common.dto.ProblemDetails;
4+
import jakarta.ws.rs.core.Response;
5+
6+
/** Utility class for building RFC 7807 ProblemDetails HTTP responses. */
7+
public class ProblemDetailsResponseBuilder {
8+
9+
private static final String PROBLEM_JSON_TYPE = "application/problem+json";
10+
11+
public static Response badRequest(String detail) {
12+
return buildResponse(400, "Bad Request", detail);
13+
}
14+
15+
public static Response unauthorized(String detail) {
16+
return buildResponse(401, "Unauthorized", detail);
17+
}
18+
19+
public static Response forbidden(String detail) {
20+
return buildResponse(403, "Forbidden", detail);
21+
}
22+
23+
public static Response notFound(String detail) {
24+
return buildResponse(404, "Not Found", detail);
25+
}
26+
27+
public static Response conflict(String detail) {
28+
return buildResponse(409, "Conflict", detail);
29+
}
30+
31+
public static Response internalServerError(String detail) {
32+
return buildResponse(500, "Internal Server Error", detail);
33+
}
34+
35+
private static Response buildResponse(int status, String title, String detail) {
36+
ProblemDetails problemDetails = new ProblemDetails(status, title, detail);
37+
38+
return Response.status(status)
39+
.entity(problemDetails)
40+
.header("Content-Type", PROBLEM_JSON_TYPE)
41+
.build();
42+
}
43+
}

sticker-award/src/main/java/com/datadoghq/stickerlandia/stickeraward/sticker/StickerResource.java

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.datadoghq.stickerlandia.stickeraward.sticker;
22

3+
import com.datadoghq.stickerlandia.stickeraward.common.exception.ProblemDetailsResponseBuilder;
34
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.CreateStickerRequest;
45
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.CreateStickerResponse;
56
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.GetAllStickersResponse;
@@ -75,7 +76,8 @@ public Response createSticker(@NotNull CreateStickerRequest data) {
7576
public Response getStickerMetadata(@PathParam("stickerId") String stickerId) {
7677
StickerDTO metadata = stickerRepository.getStickerMetadata(stickerId);
7778
if (metadata == null) {
78-
return Response.status(Response.Status.NOT_FOUND).build();
79+
return ProblemDetailsResponseBuilder.notFound(
80+
"Sticker with ID " + stickerId + " not found");
7981
}
8082
return Response.ok(metadata).build();
8183
}
@@ -96,7 +98,8 @@ public Response updateStickerMetadata(
9698
@PathParam("stickerId") String stickerId, @NotNull UpdateStickerRequest data) {
9799
StickerDTO updated = stickerRepository.updateStickerMetadata(stickerId, data);
98100
if (updated == null) {
99-
return Response.status(Response.Status.NOT_FOUND).build();
101+
return ProblemDetailsResponseBuilder.notFound(
102+
"Sticker with ID " + stickerId + " not found");
100103
}
101104
return Response.ok(updated).build();
102105
}
@@ -114,13 +117,13 @@ public Response deleteSticker(@PathParam("stickerId") String stickerId) {
114117
try {
115118
boolean deleted = stickerRepository.deleteSticker(stickerId);
116119
if (!deleted) {
117-
return Response.status(Response.Status.NOT_FOUND).build();
120+
return ProblemDetailsResponseBuilder.notFound(
121+
"Sticker with ID " + stickerId + " not found");
118122
}
119123
return Response.noContent().build();
120124
} catch (IllegalStateException e) {
121-
return Response.status(Response.Status.BAD_REQUEST)
122-
.entity("Cannot delete sticker that is assigned to users")
123-
.build();
125+
return ProblemDetailsResponseBuilder.badRequest(
126+
"Cannot delete sticker that is assigned to users");
124127
}
125128
}
126129

@@ -137,22 +140,21 @@ public Response deleteSticker(@PathParam("stickerId") String stickerId) {
137140
public Response getStickerImage(@PathParam("stickerId") String stickerId) {
138141
StickerDTO metadata = stickerRepository.getStickerMetadata(stickerId);
139142
if (metadata == null) {
140-
return Response.status(Response.Status.NOT_FOUND).build();
143+
return ProblemDetailsResponseBuilder.notFound(
144+
"Sticker with ID " + stickerId + " not found");
141145
}
142146

143147
if (metadata.getImageKey() == null) {
144-
return Response.status(Response.Status.NOT_FOUND)
145-
.entity("No image found for this sticker")
146-
.build();
148+
return ProblemDetailsResponseBuilder.notFound(
149+
"No image found for sticker " + stickerId);
147150
}
148151

149152
try {
150153
InputStream imageStream = stickerImageService.getImage(metadata.getImageKey());
151154
return Response.ok(imageStream).type("image/png").build();
152155
} catch (Exception e) {
153-
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
154-
.entity("Failed to retrieve image")
155-
.build();
156+
return ProblemDetailsResponseBuilder.internalServerError(
157+
"Failed to retrieve image for sticker " + stickerId);
156158
}
157159
}
158160

@@ -172,7 +174,8 @@ public Response uploadStickerImage(
172174
@PathParam("stickerId") String stickerId, @NotNull InputStream data) {
173175
StickerDTO metadata = stickerRepository.getStickerMetadata(stickerId);
174176
if (metadata == null) {
175-
return Response.status(Response.Status.NOT_FOUND).build();
177+
return ProblemDetailsResponseBuilder.notFound(
178+
"Sticker with ID " + stickerId + " not found");
176179
}
177180

178181
try {
@@ -195,9 +198,8 @@ public Response uploadStickerImage(
195198
return Response.ok(response).build();
196199
} catch (Exception e) {
197200
System.out.println(e);
198-
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
199-
.entity("Failed to upload image")
200-
.build();
201+
return ProblemDetailsResponseBuilder.internalServerError(
202+
"Failed to upload image for sticker " + stickerId);
201203
}
202204
}
203205
}

0 commit comments

Comments
 (0)