Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Production-oriented JSON-RPC 2.0 server library for Java, with optional Spring W
- Spring WebMVC transport adapter and Spring Boot auto-configuration
- Multiple registration styles (annotation/manual/typed)
- Explicit extension points (parser/validator/invoker/exception mapping/interceptors/metrics)
- Response-side protocol utilities for bidirectional transports (classifier/parser/validator)
- Response-side protocol utilities for bidirectional transports (envelope classifier, error-code classifier, parser,
validator)
- Focused dependency surface (no direct Guava, Commons Lang3, or Jakarta Validation dependency)

## Specification
Expand Down
10 changes: 10 additions & 0 deletions docs/protocol-and-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Implementation constants are in `JsonRpcErrorCode` and messages in `JsonRpcConst
`jsonrpc-core` also provides response-side protocol utilities:

- `JsonRpcEnvelopeClassifier`
- `JsonRpcErrorClassifier`
- `JsonRpcResponseParser`
- `JsonRpcResponseValidator`
- `JsonRpcResponseValidationOptions`
Expand All @@ -61,6 +62,15 @@ members during raw JSON parsing.
These APIs are transport-agnostic and useful for bidirectional channels (for example WebSocket) where
request/response envelopes may arrive on the same connection.

`JsonRpcErrorClassifier` interprets integer `error.code` values as:

- `STANDARD`: `-32700`, `-32600`, `-32601`, `-32602`, `-32603`
- `SERVER_RESERVED_RANGE`: `-32099..-32000`
- `CUSTOM`: any other integer value

`CUSTOM` does not mean the code is invalid. It only means the code is outside the standard set and the reserved
server-error range.

### Default Validation Rules (RFC MUST)

By default, `JsonRpcResponseValidationOptions.defaults()` enforces:
Expand Down
15 changes: 15 additions & 0 deletions docs/pure-java-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,12 @@ import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import com.limehee.jsonrpc.core.DefaultJsonRpcEnvelopeClassifier;
import com.limehee.jsonrpc.core.DefaultJsonRpcErrorClassifier;
import com.limehee.jsonrpc.core.DefaultJsonRpcResponseParser;
import com.limehee.jsonrpc.core.DefaultJsonRpcResponseValidator;
import com.limehee.jsonrpc.core.JsonRpcEnvelopeClassifier;
import com.limehee.jsonrpc.core.JsonRpcErrorClassifier;
import com.limehee.jsonrpc.core.JsonRpcErrorCodeCategory;
import com.limehee.jsonrpc.core.JsonRpcEnvelopeType;
import com.limehee.jsonrpc.core.JsonRpcIncomingResponse;
import com.limehee.jsonrpc.core.JsonRpcIncomingResponseEnvelope;
Expand All @@ -361,6 +364,7 @@ import com.limehee.jsonrpc.core.JsonRpcResponseValidator;

ObjectMapper mapper = JsonMapper.builder().build();
JsonRpcEnvelopeClassifier classifier = new DefaultJsonRpcEnvelopeClassifier();
JsonRpcErrorClassifier errorClassifier = new DefaultJsonRpcErrorClassifier();
JsonRpcResponseParser responseParser = new DefaultJsonRpcResponseParser();
JsonRpcResponseValidator responseValidator = new DefaultJsonRpcResponseValidator();

Expand All @@ -373,6 +377,11 @@ if (envelopeType == JsonRpcEnvelopeType.REQUEST) {
JsonRpcIncomingResponseEnvelope envelope = responseParser.parse(payload);
for (JsonRpcIncomingResponse response : envelope.responses()) {
responseValidator.validate(response);
if (response.errorPresent() && response.error() != null && response.error().has("code")) {
int code = response.error().get("code").asInt();
JsonRpcErrorCodeCategory category = errorClassifier.classify(code);
System.out.println("error.code category = " + category);
}
// route by response.id() to pending-call registry
}
} else {
Expand All @@ -383,6 +392,12 @@ if (envelopeType == JsonRpcEnvelopeType.REQUEST) {
For policy tuning, customize `JsonRpcResponseValidationOptions` and pass it into
`DefaultJsonRpcResponseValidator`.

`JsonRpcErrorClassifier` categories:

- `STANDARD`: one of `-32700`, `-32600`, `-32601`, `-32602`, `-32603`
- `SERVER_RESERVED_RANGE`: inside `-32099..-32000`
- `CUSTOM`: any other integer code

## 10. Concurrency Notes

- `JsonRpcDispatcher` invocation path is stateless per request except method registry lookups.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.limehee.jsonrpc.core;

/**
* Default classifier for integer JSON-RPC {@code error.code} values.
*/
public class DefaultJsonRpcErrorClassifier implements JsonRpcErrorClassifier {

/**
* {@inheritDoc}
*/
@Override
public JsonRpcErrorCodeCategory classify(int code) {
if (code == JsonRpcErrorCode.PARSE_ERROR
|| code == JsonRpcErrorCode.INVALID_REQUEST
|| code == JsonRpcErrorCode.METHOD_NOT_FOUND
|| code == JsonRpcErrorCode.INVALID_PARAMS
|| code == JsonRpcErrorCode.INTERNAL_ERROR) {
return JsonRpcErrorCodeCategory.STANDARD;
}
if (code >= -32099 && code <= -32000) {
return JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE;
}
return JsonRpcErrorCodeCategory.CUSTOM;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@

/**
* Default validator for parsed incoming JSON-RPC responses.
* <p>
* Integer {@code error.code} validation uses {@link JsonRpcErrorClassifier} for standard and server-range policies.
* {@link JsonRpcResponseErrorCodePolicy#CUSTOM_RANGE} remains a direct numeric bounds check and does not depend on the
* classifier's category result.
* </p>
*/
public class DefaultJsonRpcResponseValidator implements JsonRpcResponseValidator {

private static final JsonRpcErrorClassifier ERROR_CLASSIFIER = new DefaultJsonRpcErrorClassifier();

private final JsonRpcResponseValidationOptions options;

/**
Expand Down Expand Up @@ -148,55 +155,26 @@ private JsonRpcException invalidRequest() {
* @param code integer error code
*/
private void validateErrorCodePolicy(int code) {
JsonRpcResponseErrorCodePolicy policy = options.errorCodePolicy();
if (policy == JsonRpcResponseErrorCodePolicy.ANY_INTEGER) {
return;
}
if (policy == JsonRpcResponseErrorCodePolicy.STANDARD_ONLY) {
if (!isStandardErrorCode(code)) {
throw invalidRequest();
switch (options.errorCodePolicy()) {
case ANY_INTEGER -> {
}
return;
}
if (policy == JsonRpcResponseErrorCodePolicy.STANDARD_OR_SERVER_ERROR_RANGE) {
if (!isStandardErrorCode(code) && !isServerErrorRangeCode(code)) {
throw invalidRequest();
case STANDARD_ONLY -> {
if (!ERROR_CLASSIFIER.isStandard(code)) {
throw invalidRequest();
}
}
return;
}
if (policy == JsonRpcResponseErrorCodePolicy.CUSTOM_RANGE) {
Integer min = options.errorCodeRangeMin();
Integer max = options.errorCodeRangeMax();
if (min == null || max == null) {
throw invalidRequest();
case STANDARD_OR_SERVER_ERROR_RANGE -> {
if (!ERROR_CLASSIFIER.isStandard(code) && !ERROR_CLASSIFIER.isServerErrorRange(code)) {
throw invalidRequest();
}
}
if (code < min || code > max) {
throw invalidRequest();
case CUSTOM_RANGE -> {
Integer min = options.errorCodeRangeMin();
Integer max = options.errorCodeRangeMax();
if (min == null || max == null || code < min || code > max) {
throw invalidRequest();
}
}
}
}

/**
* Checks whether a code is one of the JSON-RPC standard error codes.
*
* @param code integer error code
* @return {@code true} when code is standard
*/
private boolean isStandardErrorCode(int code) {
return code == JsonRpcErrorCode.PARSE_ERROR
|| code == JsonRpcErrorCode.INVALID_REQUEST
|| code == JsonRpcErrorCode.METHOD_NOT_FOUND
|| code == JsonRpcErrorCode.INVALID_PARAMS
|| code == JsonRpcErrorCode.INTERNAL_ERROR;
}

/**
* Checks whether a code belongs to the JSON-RPC server-error reserved range.
*
* @param code integer error code
* @return {@code true} when code is within {@code -32099..-32000}
*/
private boolean isServerErrorRangeCode(int code) {
return code >= -32099 && code <= -32000;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.limehee.jsonrpc.core;

/**
* Classifies integer JSON-RPC {@code error.code} values into semantic categories.
* <p>
* This utility describes what a code represents. It does not decide whether a response should be accepted or rejected
* under a validation policy.
* </p>
*/
public interface JsonRpcErrorClassifier {

/**
* Classifies the provided integer error code.
*
* @param code integer JSON-RPC error code
* @return semantic error-code category
*/
JsonRpcErrorCodeCategory classify(int code);

/**
* Checks whether the provided code is one of the JSON-RPC standard error codes.
*
* @param code integer JSON-RPC error code
* @return {@code true} when the code belongs to the standard set
*/
default boolean isStandard(int code) {
return classify(code) == JsonRpcErrorCodeCategory.STANDARD;
}

/**
* Checks whether the provided code belongs to the server-reserved range.
*
* @param code integer JSON-RPC error code
* @return {@code true} when the code is within {@code -32099..-32000}
*/
default boolean isServerErrorRange(int code) {
return classify(code) == JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE;
}

/**
* Checks whether the provided code is neither standard nor server-reserved.
*
* @param code integer JSON-RPC error code
* @return {@code true} when the code falls into the custom category
*/
default boolean isCustom(int code) {
return classify(code) == JsonRpcErrorCodeCategory.CUSTOM;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.limehee.jsonrpc.core;

/**
* Semantic category for an integer JSON-RPC {@code error.code}.
*/
public enum JsonRpcErrorCodeCategory {

/**
* One of the JSON-RPC 2.0 standard error codes.
*/
STANDARD,

/**
* Value inside the server-reserved range {@code -32099..-32000}.
*/
SERVER_RESERVED_RANGE,

/**
* Any integer outside the standard set and the server-reserved range.
* <p>
* This category does not mean the code is invalid. It only means the code is application-defined or otherwise
* outside the standard and reserved sets.
* </p>
*/
CUSTOM
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.limehee.jsonrpc.core;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class DefaultJsonRpcErrorClassifierTest {

private final JsonRpcErrorClassifier classifier = new DefaultJsonRpcErrorClassifier();

@Test
void classifyReturnsStandardForAllStandardJsonRpcErrorCodes() {
assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.PARSE_ERROR));
assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.INVALID_REQUEST));
assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.METHOD_NOT_FOUND));
assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.INVALID_PARAMS));
assertSame(JsonRpcErrorCodeCategory.STANDARD, classifier.classify(JsonRpcErrorCode.INTERNAL_ERROR));
}

@Test
void classifyReturnsServerReservedRangeForBoundaryValues() {
assertSame(JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE, classifier.classify(-32099));
assertSame(JsonRpcErrorCodeCategory.SERVER_RESERVED_RANGE, classifier.classify(-32000));
}

@Test
void classifyReturnsCustomForAllNonStandardNonReservedValues() {
assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(-32100));
assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(-31999));
assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(-40000));
assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(0));
assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(1));
assertSame(JsonRpcErrorCodeCategory.CUSTOM, classifier.classify(1001));
}

@Test
void helperMethodsReflectClassificationResult() {
assertTrue(classifier.isStandard(JsonRpcErrorCode.METHOD_NOT_FOUND));
assertFalse(classifier.isServerErrorRange(JsonRpcErrorCode.METHOD_NOT_FOUND));
assertFalse(classifier.isCustom(JsonRpcErrorCode.METHOD_NOT_FOUND));

assertTrue(classifier.isServerErrorRange(-32050));
assertFalse(classifier.isStandard(-32050));
assertFalse(classifier.isCustom(-32050));

assertTrue(classifier.isCustom(1001));
assertFalse(classifier.isStandard(1001));
assertFalse(classifier.isServerErrorRange(1001));
}
}
Loading