Skip to content

Commit cab3a61

Browse files
committed
Extend post-processing for type field
1 parent 790ae4a commit cab3a61

File tree

46 files changed

+929
-398
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+929
-398
lines changed

README.md

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ declaratively map exception fields to a `Problem`.
159159
To extract values from target exception, it's possible to use placeholders for interpolation.
160160

161161
- `{message}` - the exact `getMessage()` result from your exception,
162-
- `{traceId}` - the `context.getTraceId()` result for tracking error response with the actual request. The `context` is
162+
- `{context.traceId}` - the `context.getTraceId()` result for tracking error response with the actual request. The `context` is
163163
something that is build in `@RestControllerAdvice`s and it contains processing metadata. Currently only `traceId` is
164164
supported,
165165
- `{fieldName}` - any field name declared in exceptions and its superclasses (scanned from current class to its most
@@ -184,7 +184,7 @@ To extract values from target exception, it's possible to use placeholders for i
184184
title = "Invalid Request",
185185
status = 400,
186186
detail = "{message}: {fieldName}",
187-
instance = "https://example.org/instances/{traceId}",
187+
instance = "https://example.org/instances/{context.traceId}",
188188
extensions = {"userId", "fieldName"})
189189
public class ExampleException extends RuntimeException {
190190

@@ -237,7 +237,7 @@ public class MaxUploadSizeExceededResolver implements ProblemResolver {
237237
```
238238

239239
`ProblemResolver` implementations return a `ProblemBuilder` for flexibility in constructing the final `Problem` object.
240-
It's a convenience for additional `Problem` processing (such as `instance-override` feature).
240+
It's a convenience method for further extending `Problem` object by processing downstream.
241241

242242
#### Custom `@RestControllerAdvice`
243243

@@ -257,36 +257,40 @@ If you want your advice to override the ones provided by this library, use a sma
257257
| `ProblemException` | `Ordered.LOWEST_PRECEDENCE - 10` |
258258
| `Exception` | `Ordered.LOWEST_PRECEDENCE` |
259259

260-
While implementing custom `@ControllerAdvice`, enforcing `instance-override` feature must be performed manually. Value
261-
of `"instance"` field will come from request attributes and must be overwritten if not `null`.
260+
While implementing custom `@ControllerAdvice`, don't forget of calling `ProblemPostProcessor` manually, before returning
261+
`Problem` object.
262262

263263
```java
264264
@Order(Ordered.LOWEST_PRECEDENCE - 20)
265265
@Component
266266
@RestControllerAdvice
267267
public class ExampleExceptionAdvice {
268268

269+
private final ProblemPostProcessor problemPostProcessor;
270+
271+
// constructor
272+
269273
@ExceptionHandler(ExampleException.class)
270274
public ResponseEntity<Problem> handleExampleException(ExampleException ex, WebRequest request) {
271-
ProblemBuilder =
275+
ProblemContext context = (ProblemContext) request.getAttribute(PROBLEM_CONTEXT, SCOPE_REQUEST);
276+
if (context == null) {
277+
context = ProblemContext.empty();
278+
}
279+
280+
HttpHeaders headers = new HttpHeaders();
281+
headers.setContentType(MediaType.APPLICATION_PROBLEM_JSON);
282+
283+
Problem problem =
272284
Problem.builder()
273285
.type("https://example.org/errors/invalid-request")
274286
.title("Invalid Request")
275287
.status(400)
276288
.detail(ex.getMessage())
277289
.instance("https://example.org/instances/" + context.getTraceId())
278290
.extension("userId", e.getUserId())
279-
.extension("fieldName", e.getFieldName());
280-
281-
Object instanceOverride = request.getAttribute(TracingSupport.INSTANCE_OVERRIDE, SCOPE_REQUEST);
282-
if (instanceOverride != null) {
283-
builder = builder.instance(instanceOverride.toString());
284-
}
285-
286-
Problem problem = builder.build();
287-
288-
HttpHeaders headers = new HttpHeaders();
289-
headers.setContentType(MediaType.APPLICATION_PROBLEM_JSON);
291+
.extension("fieldName", e.getFieldName())
292+
.build();
293+
problem = problemPostProcessor.process(context, problem);
290294

291295
HttpStatus status = ProblemSupport.resolveStatus(problem.getStatus());
292296

@@ -538,24 +542,62 @@ Library can be configured with following properties.
538542

539543
### `problem4j.detail-format`
540544

541-
Property that specifies how exception handling imported with this module should print `"detail"` field of `Problem`
542-
model (`lowercase`, **`capitalized` - default**, `uppercase`). Useful for keeping the same style of errors coming from
543-
library and your application.
545+
Property that specifies how exception handling imported with this module should print the `"detail"` field of the
546+
`Problem` model (`lowercase`, **`capitalized` - default**, `uppercase`). Useful for keeping a consistent style between
547+
errors generated by the library and those from your application.
544548

545549
### `problem4j.tracing-header-name`
546550

547551
Property that specifies the name of the HTTP header used for tracing requests. If set, the trace identifier from this
548-
header can be injected into the `Problem` response, for example into the`"instance"` field when combined with
549-
[`problem4j.instance-override`](#problem4jinstance-override). Defaults to `null` (disabled).
552+
header is extracted and made available within the request context (`ProblemContext`). This value can be referenced in
553+
other configuration properties using the `{context.traceId}` placeholder. Defaults to `null` (disabled).
554+
555+
### `problem4j.type-override`
556+
557+
This property allow overriding the `"type"` field of `Problem` responses with custom templates at the environment level.
558+
This is useful when the final URIs are not known at development time or may vary depending on deployment, enabling them
559+
to resolve to proper HTTP links dynamically.
560+
561+
Property that defines a template for overriding the `"type"` field in `Problem` responses. The value may contain special
562+
placeholders that will be replaced at runtime:
563+
564+
- `{problem.type}` — the original problem’s `"type"` value
565+
- `{context.traceId}` — the current request’s trace identifier (if available)
566+
567+
Defaults to `null` (no override applied). This can be used to unify or enrich problem type URIs across the application.
568+
569+
> For example, if setting `type` in your exception to `problems/validation` and:
570+
>
571+
> ```properties
572+
> problem4j.type-override=https://errors.example.com/{problem.type}
573+
> ```
574+
>
575+
> the post processor will change `"type"` value to `"https://errors.example.com/problems/validation`.
550576
551577
### `problem4j.instance-override`
552578
553-
Property that defines a template for overriding the `"instance"` field in `Problem` responses.The value may contain the
554-
special placeholder `{traceId}`, which will be replaced at runtime with the trace identifier from the current request (
555-
see [`problem4j.tracing-header-name`](#problem4jtracing-header-name)). Defaults to `null` (no override applied).
579+
This property allow overriding the `"instance"` field of `Problem` responses with custom templates at the environment
580+
level. This is useful when the final URIs are not known at development time or may vary depending on deployment,
581+
enabling them to resolve to proper HTTP links dynamically.
582+
583+
Property that defines a template for overriding the `"instance"` field in `Problem` responses. The value may contain
584+
special placeholders that will be replaced at runtime:
585+
586+
- `{problem.instance}` — the original problem’s `"instance"` value
587+
- `{context.traceId}` — the current request’s trace identifier (if available)
588+
589+
Defaults to `null` (no override applied). This is useful for controlling how problem instances are represented in your
590+
API responses, even without tracing enabled.
556591
557-
For example, by assigning `problem4j.instance-override=/error-instances/{traceId}`, with tracing enabled, each `Problem`
558-
response will have `"instance"` field matching to that format (e.g. `"/error-instances/WQ1tbs12rtSD"`).
592+
> For example if not assigning `instance` at all in your exceptions and:
593+
>
594+
> ```properties
595+
> problem4j.instance-override=/errors/{context.traceId}
596+
> ```
597+
>
598+
> the post processor will change `"instance"` value to `"/errors/WQ1tbs12rtSD"`.
599+
>
600+
> For using `{problem.instance}` placeholder, the `instance` field will behave similarly to `type-override`.
559601
560602
### `problem4j.resolver-caching.enabled`
561603

problem4j-spring-web/src/main/java/io/github/malczuuu/problem4j/spring/web/ProblemConfiguration.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import io.github.malczuuu.problem4j.spring.web.annotation.ProblemMappingProcessor;
66
import io.github.malczuuu.problem4j.spring.web.format.DefaultProblemFormat;
77
import io.github.malczuuu.problem4j.spring.web.format.ProblemFormat;
8+
import io.github.malczuuu.problem4j.spring.web.processor.OverridingProblemPostProcessor;
9+
import io.github.malczuuu.problem4j.spring.web.processor.ProblemPostProcessor;
810
import io.github.malczuuu.problem4j.spring.web.resolver.ProblemResolver;
911
import io.github.malczuuu.problem4j.spring.web.resolver.ProblemResolverConfiguration;
1012
import java.util.List;
@@ -43,6 +45,33 @@ public ProblemFormat problemFormat(ProblemProperties properties) {
4345
return new DefaultProblemFormat(properties.getDetailFormat());
4446
}
4547

48+
/**
49+
* Provides a {@link ProblemPostProcessor} that applies post-processing rules to {@code Problem}
50+
* instances before they are returned in HTTP responses.
51+
*
52+
* <p>The default implementation, {@link OverridingProblemPostProcessor}, supports configurable
53+
* overrides for problem fields such as {@code type} and {@code instance}, based on the properties
54+
* defined in {@link ProblemProperties}. These overrides may include runtime placeholders such as:
55+
*
56+
* <ul>
57+
* <li>{@code {problem.type}} — replaced with the original problem’s type URI
58+
* <li>{@code {problem.instance}} — replaced with the original problem’s instance URI
59+
* <li>{@code {context.traceId}} — replaced with the current trace identifier, if available
60+
* </ul>
61+
*
62+
* <p>This allows enriching or normalizing problem responses without modifying the original
63+
* exception mapping logic.
64+
*
65+
* @param properties the configuration properties containing override templates and settings
66+
* @return a new {@link OverridingProblemPostProcessor} instance
67+
* @see io.github.malczuuu.problem4j.core.Problem
68+
*/
69+
@ConditionalOnMissingBean(ProblemPostProcessor.class)
70+
@Bean
71+
public ProblemPostProcessor problemPostProcessor(ProblemProperties properties) {
72+
return new OverridingProblemPostProcessor(properties);
73+
}
74+
4675
/**
4776
* Provides a {@link ProblemResolverStore} that aggregates all {@link ProblemResolver}
4877
* implementations.

problem4j-spring-web/src/main/java/io/github/malczuuu/problem4j/spring/web/ProblemProperties.java

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.github.malczuuu.problem4j.spring.web;
22

3+
import io.github.malczuuu.problem4j.spring.web.context.ProblemContextSettings;
4+
import io.github.malczuuu.problem4j.spring.web.processor.PostProcessorSettings;
35
import org.springframework.boot.context.properties.ConfigurationProperties;
46
import org.springframework.boot.context.properties.bind.DefaultValue;
57

@@ -9,10 +11,11 @@
911
* <p>These properties can be set under the {@code problem4j.*} prefix.
1012
*/
1113
@ConfigurationProperties(prefix = "problem4j")
12-
public class ProblemProperties {
14+
public class ProblemProperties implements ProblemContextSettings, PostProcessorSettings {
1315

1416
private final String detailFormat;
1517
private final String tracingHeaderName;
18+
private final String typeOverride;
1619
private final String instanceOverride;
1720

1821
private final ResolverCaching resolverCaching;
@@ -23,18 +26,20 @@ public class ProblemProperties {
2326
* @param detailFormat format for the "detail" field (one of {@link DetailFormat#LOWERCASE},
2427
* {@link DetailFormat#CAPITALIZED}, {@link DetailFormat#UPPERCASE})
2528
* @param tracingHeaderName name of the HTTP header carrying a trace ID (nullable)
26-
* @param instanceOverride template for overriding the "instance" field; may contain "{traceId}"
27-
* placeholder (nullable)
29+
* @param instanceOverride template for overriding the "instance" field; may contain
30+
* "{context.traceId}" placeholder (nullable)
2831
* @param resolverCaching caching for resolver lookups ({@link CachingProblemResolverStore});
2932
* defaults to {@link ResolverCaching#createDefault()}
3033
*/
3134
public ProblemProperties(
3235
@DefaultValue(DetailFormat.CAPITALIZED) String detailFormat,
3336
String tracingHeaderName,
37+
String typeOverride,
3438
String instanceOverride,
3539
ResolverCaching resolverCaching) {
3640
this.detailFormat = detailFormat;
3741
this.tracingHeaderName = tracingHeaderName;
42+
this.typeOverride = typeOverride;
3843
this.instanceOverride = instanceOverride;
3944
this.resolverCaching =
4045
resolverCaching != null ? resolverCaching : ResolverCaching.createDefault();
@@ -60,24 +65,56 @@ public String getDetailFormat() {
6065
*
6166
* @return the tracing header name, or {@code null} if not set
6267
*/
68+
@Override
6369
public String getTracingHeaderName() {
6470
return tracingHeaderName;
6571
}
6672

73+
/**
74+
* Returns the configured type override.
75+
*
76+
* <p>This value defines a fixed or templated {@code type} URI to be used for all problems
77+
* processed by the post-processor, replacing the original problem type if present. The value may
78+
* include special placeholders that will be dynamically replaced at runtime:
79+
*
80+
* <ul>
81+
* <li>{@code {problem.type}} — replaced with the original problem’s type URI
82+
* <li>{@code {context.traceId}} — replaced with the current trace identifier from the {@code
83+
* ProblemContext}
84+
* </ul>
85+
*
86+
* <p>This allows flexible configuration of problem types depending on context or trace
87+
* information. If no override is configured, this method may return {@code null}, and the
88+
* original problem type will be preserved.
89+
*
90+
* @return the configured type override string, or {@code null} if not set
91+
* @see io.github.malczuuu.problem4j.spring.web.context.ProblemContext
92+
*/
93+
@Override
94+
public String getTypeOverride() {
95+
return typeOverride;
96+
}
97+
6798
/**
6899
* Returns the configured instance override.
69100
*
70-
* <p>This value may contain the special placeholder {@code {traceId}}, which will be replaced at
71-
* runtime with the current trace identifier from the {@link
72-
* io.github.malczuuu.problem4j.spring.web.context.ProblemContext}. If no override is configured,
73-
* this method may return {@code null}.
101+
* <p>This value may contain special placeholders that will be replaced at runtime with contextual
102+
* or problem-specific data:
103+
*
104+
* <ul>
105+
* <li>{@code {problem.instance}} — replaced with the original problem’s instance URI
106+
* <li>{@code {context.traceId}} — replaced with the current trace identifier from the {@code
107+
* ProblemContext}
108+
* </ul>
74109
*
75-
* <p>This is useful if {@code instance} field will not be known while throwing {@code
76-
* ProblemException} (or {@code @ProblemMapping}-annotated exception). Setting this configuration,
77-
* along with {@link #tracingHeaderName} will enable this feature.
110+
* <p>This is useful if the {@code instance} field cannot be determined when throwing a {@code
111+
* ProblemException} (or an exception annotated with {@code @ProblemMapping}). Setting this
112+
* configuration, along with {@link #tracingHeaderName}, enables this feature.
78113
*
79114
* @return the configured instance override string, or {@code null} if not set
115+
* @see io.github.malczuuu.problem4j.spring.web.context.ProblemContext
80116
*/
117+
@Override
81118
public String getInstanceOverride() {
82119
return instanceOverride;
83120
}

problem4j-spring-web/src/main/java/io/github/malczuuu/problem4j/spring/web/annotation/DefaultProblemMappingProcessor.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
* <li>Interpolate placeholders of the form {@code {name}}:
2222
* <ul>
2323
* <li>{@code {message}} -> {@link Throwable#getMessage()}
24-
* <li>{@code {traceId}} -> {@link ProblemContext#getTraceId()} (special shorthand)
24+
* <li>{@code {context.traceId}} -> {@link ProblemContext#getTraceId()} (special
25+
* shorthand)
2526
* <li>{@code {fieldName}} -> any field in the exception class hierarchy
2627
* </ul>
2728
* <li>Ignore placeholders that resolve to null or empty string.
@@ -178,8 +179,8 @@ private void applyExtensionsOnBuilder(
178179
}
179180

180181
/**
181-
* Interpolate placeholders of form {name}. Special forms: - {message} - {context.key} - {traceId}
182-
* (shorthand for {context.traceId}) - other names: looks for instance field.
182+
* Interpolate placeholders of form {name}. Special forms: - {message} - {context.key} -
183+
* {context.traceId} (shorthand for {context.traceId}) - other names: looks for instance field.
183184
*
184185
* <p>If a placeholder resolves to null - it's replaced by empty string.
185186
*/

problem4j-spring-web/src/main/java/io/github/malczuuu/problem4j/spring/web/annotation/ProblemMapping.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
* <ul>
1919
* <li>{@code {message}} -> the exception's {@link Throwable#getMessage()}
20-
* <li>{@code {traceId}} -> the traceId from {@code ProblemContext#getTraceId()} (special
20+
* <li>{@code {context.traceId}} -> the traceId from {@code ProblemContext#getTraceId()} (special
2121
* shorthand)
2222
* <li>{@code {fieldName}} -> value of any field (private or public) in the exception class
2323
* hierarchy
@@ -52,7 +52,7 @@
5252
* type = "https://example.org/errors/validation",
5353
* title = "Validation Failed",
5454
* status = 400,
55-
* detail = "Invalid input for user {userId}, trace {traceId}",
55+
* detail = "Invalid input for user {userId}, trace {context.traceId}",
5656
* extensions = {"userId", "fieldName"}
5757
* )
5858
* public class ValidationException extends RuntimeException {
@@ -85,7 +85,7 @@
8585
*
8686
* <ul>
8787
* <li>{@code {message}} -> exception message
88-
* <li>{@code {traceId}} -> trace ID from {@code ProblemContext}
88+
* <li>{@code {context.traceId}} -> trace ID from {@code ProblemContext}
8989
* <li>{@code {fieldName}} -> value of any field in the exception class hierarchy
9090
* </ul>
9191
*

problem4j-spring-web/src/main/java/io/github/malczuuu/problem4j/spring/web/annotation/ProblemMappingProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public interface ProblemMappingProcessor {
3232
Pattern PLACEHOLDER = Pattern.compile("\\{([^}]+)}");
3333

3434
String MESSAGE_LABEL = "message";
35-
String TRACE_ID_LABEL = "traceId";
35+
String TRACE_ID_LABEL = "context.traceId";
3636

3737
/**
3838
* Convert {@link Throwable} -> {@link ProblemBuilder} according to its {@link ProblemMapping}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.github.malczuuu.problem4j.spring.web.context;
2+
3+
import java.util.UUID;
4+
5+
/**
6+
* Utility class providing constants and helper methods for tracing support within the Problem4J.
7+
*/
8+
public final class ContextSupport {
9+
10+
/** Request attribute key used to store a trace identifier. */
11+
public static final String TRACE_ID =
12+
"io.github.malczuuu.problem4j.spring.web.context.ProblemContext.traceId";
13+
14+
/**
15+
* Request attribute key used to store the {@link ProblemContext} object associated with the
16+
* current request. It allows sharing contextual information (such as trace identifiers or
17+
* additional diagnostic data) between components involved in problem handling.
18+
*/
19+
public static final String PROBLEM_CONTEXT =
20+
"io.github.malczuuu.problem4j.spring.web.context.ProblemContext";
21+
22+
/**
23+
* Generates a random trace identifier in {@code urn:uuid:<uuid>} format.
24+
*
25+
* @return generated trace identifier
26+
*/
27+
public static String getRandomTraceId() {
28+
return "urn:uuid:" + UUID.randomUUID();
29+
}
30+
31+
private ContextSupport() {}
32+
}

0 commit comments

Comments
 (0)