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
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import com.predic8.membrane.core.http.Response;
import com.predic8.membrane.core.interceptor.AbstractInterceptor;
import com.predic8.membrane.core.interceptor.Outcome;
import com.predic8.membrane.core.interceptor.json.rpc.JsonRPCValidator.PayloadType;
import com.predic8.membrane.core.interceptor.json.rpc.JsonRPCValidator.RequestValidationResult;
import com.predic8.membrane.core.interceptor.json.rpc.JsonRPCValidator.ResponseValidationContext;
import com.predic8.membrane.core.interceptor.json.rpc.JsonRPCValidator.ValidationError;
Expand All @@ -37,13 +36,12 @@
import static com.predic8.membrane.core.http.Response.statusCode;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.RESPONSE;
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.interceptor.json.rpc.JsonRPCValidator.PayloadType.BATCH;
import static com.predic8.membrane.core.interceptor.json.rpc.JsonRPCValidator.PayloadType.SINGLE;
import static com.predic8.membrane.core.interceptor.json.rpc.JsonRPCValidator.getPayloadType;
import static java.util.EnumSet.of;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.ERR_INVALID_REQUEST;
import static java.util.EnumSet.of;

/**
* @topic 3. Security and Validation
Expand Down Expand Up @@ -158,7 +156,7 @@ private Outcome rejectRequest(Exchange exc, ValidationError error) {
}
log.info("Rejected JSON-RPC request: {}", error.message());
exc.setResponse(createErrorResponse(error));
return RETURN;
return ABORT;
}

private Outcome rejectResponse(Exchange exc, ValidationError error) {
Expand All @@ -167,7 +165,7 @@ private Outcome rejectResponse(Exchange exc, ValidationError error) {
}
log.info("Rejected JSON-RPC response: {}", error.message());
exc.setResponse(createErrorResponse(error));
return RETURN;
return ABORT;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@
import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON;
import static com.predic8.membrane.core.http.Request.METHOD_POST;
import static com.predic8.membrane.core.http.Response.statusCode;
import static com.predic8.membrane.core.jsonrpc.JSONRPCRequest.parse;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.RESPONSE;
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.jsonrpc.JSONRPCRequest.parse;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.ERR_INVALID_REQUEST;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.error;
import static com.predic8.membrane.core.mcp.MCPToolsList.METHOD;
Expand Down Expand Up @@ -232,10 +232,10 @@ private Outcome reject(Exchange exc, ValidationError error, boolean addAllowHead
log.info("Rejected MCP request: {}", error.message());
if (error.notification()) {
exc.setResponse(statusCode(error.httpStatus()).bodyEmpty().build());
return RETURN;
return ABORT;
}
exc.setResponse(createErrorResponse(error, addAllowHeader));
return RETURN;
return ABORT;
}

private Response createErrorResponse(ValidationError error, boolean addAllowHeader) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* Copyright 2026 predic8 GmbH, www.predic8.com

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

http://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 com.predic8.membrane.core.interceptor.json.rpc;

import com.predic8.membrane.core.interceptor.flow.invocation.AbstractInterceptorFlowTest;
import org.junit.jupiter.api.Test;

import static com.predic8.membrane.core.interceptor.flow.invocation.FlowTestInterceptors.A;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* A rejection must put the exchange into the abort flow, so that the interceptors before
* jsonRPCProtection see handleAbort instead of handleResponse. See
* {@link com.predic8.membrane.core.interceptor.flow.invocation.InterceptorFlowTest} for the
* notation: {@code >a} request flow, {@code <a} response flow, {@code ?a} abort flow.
*/
class JsonRPCProtectionAbortFlowTest extends AbstractInterceptorFlowTest {

@Test
void rejectedRequestAbortsTheFlow() throws Exception {
// The harness POSTs form-urlencoded, which jsonRPCProtection rejects as not JSON.
// assertFlow() cannot be used because the body is the JSON-RPC error envelope the plugin set.
String response = getResponse(A, new JsonRPCProtectionInterceptor());

assertTrue(response.endsWith("?a"), response);
assertFalse(response.contains("<a"), response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,9 @@
import static com.predic8.membrane.core.http.MimeType.TEXT_PLAIN;
import static com.predic8.membrane.core.http.Request.METHOD_GET;
import static com.predic8.membrane.core.http.Request.METHOD_POST;
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.ERR_INTERNAL_ERROR;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.ERR_INVALID_PARAMS;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.ERR_INVALID_REQUEST;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.ERR_METHOD_NOT_FOUND;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.*;
import static org.junit.jupiter.api.Assertions.*;

class JsonRPCProtectionInterceptorTest {
Expand Down Expand Up @@ -790,7 +787,7 @@ private static RequestCase requestRejects(String name,
method,
contentType,
body,
RETURN,
ABORT,
expectedStatus,
expectedJsonRpcCode,
expectedMessageSnippet,
Expand All @@ -814,7 +811,7 @@ private static ResponseCase responseRejects(String name,
config,
requestBody,
responseBody,
RETURN,
ABORT,
500,
ERR_INTERNAL_ERROR,
expectedMessageSnippet,
Expand Down Expand Up @@ -928,7 +925,7 @@ private record RequestCase(String name,
Object expectedId,
boolean batchErrorShape) {
private boolean expectsRejection() {
return expectedOutcome == RETURN;
return expectedOutcome == ABORT;
}

@Override
Expand All @@ -948,7 +945,7 @@ private record ResponseCase(String name,
Object expectedId,
boolean batchErrorShape) {
private boolean expectsRejection() {
return expectedOutcome == RETURN;
return expectedOutcome == ABORT;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import com.predic8.membrane.annot.yaml.parsing.GenericYamlParser;
import com.predic8.membrane.core.config.spring.GrammarAutoGenerated;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.http.Response;
import com.predic8.membrane.core.http.Request;
import com.predic8.membrane.core.http.Response;
import com.predic8.membrane.core.interceptor.Outcome;
import com.predic8.membrane.core.router.DefaultRouter;
import org.junit.jupiter.api.Test;
Expand All @@ -38,8 +38,8 @@
import static com.predic8.membrane.core.http.MimeType.TEXT_PLAIN;
import static com.predic8.membrane.core.http.Request.METHOD_GET;
import static com.predic8.membrane.core.http.Request.METHOD_POST;
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.jsonrpc.JSONRPCResponse.*;
import static org.junit.jupiter.api.Assertions.*;

Expand Down Expand Up @@ -262,7 +262,7 @@ private static RequestCase rejects(String name,
int code,
String message,
Object id) {
return new RequestCase(name, config, httpMethod, contentType, body, RETURN, status, code, message, id);
return new RequestCase(name, config, httpMethod, contentType, body, ABORT, status, code, message, id);
}

private MCPProtectionInterceptor interceptor(String config) throws Exception {
Expand Down Expand Up @@ -343,7 +343,7 @@ void suppressesJsonRpcErrorForRejectedNotification() throws Exception {
{"jsonrpc":"2.0","method":"notifications/cancelled","params":{}}
""");

assertEquals(RETURN, interceptor("methods:\n notifications: false").handleRequest(exc));
assertEquals(ABORT, interceptor("methods:\n notifications: false").handleRequest(exc));
assertNotNull(exc.getResponse());
assertEquals(403, exc.getResponse().getStatusCode());
assertTrue(exc.getResponse().getBodyAsStringDecoded().isEmpty());
Expand All @@ -355,7 +355,7 @@ void sendsJsonRpcErrorForRejectedRequestWithNullId() throws Exception {
{"jsonrpc":"2.0","id":null,"method":"resources/read","params":{}}
""");

assertEquals(RETURN, interceptor("").handleRequest(exc));
assertEquals(ABORT, interceptor("").handleRequest(exc));
assertError(
exc,
403,
Expand Down
Loading