diff --git a/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java b/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java index f68ed083506..171b242e7d8 100644 --- a/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java +++ b/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java @@ -20,6 +20,7 @@ import io.grpc.ExperimentalApi; import java.io.IOException; import java.util.List; +import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -36,11 +37,20 @@ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/5066") public class GrpcServlet extends HttpServlet { private static final long serialVersionUID = 1L; + public static final String REMOVE_CONTEXT_PATH = "REMOVE_CONTEXT_PATH"; private final ServletAdapter servletAdapter; + private boolean removeContextPath; GrpcServlet(ServletAdapter servletAdapter) { this.servletAdapter = servletAdapter; + removeContextPath = false; // default value + } + + @Override + public void init() throws ServletException { + super.init(); + removeContextPath = Boolean.parseBoolean(getInitParameter(REMOVE_CONTEXT_PATH)); } /** @@ -58,6 +68,15 @@ private static ServletAdapter loadServices(List extends BindableService> binda return serverBuilder.buildServletAdapter(); } + protected String getMethod(HttpServletRequest req) { + String method = req.getRequestURI(); + if (removeContextPath) { + // remove context path used in application server + method = method.substring(req.getContextPath().length()); + } + return method.substring(1); // remove the leading "/" + } + @Override protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -67,7 +86,7 @@ protected final void doGet(HttpServletRequest request, HttpServletResponse respo @Override protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { - servletAdapter.doPost(request, response); + servletAdapter.doPost(getMethod(request), request, response); } @Override diff --git a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java index 5a567916f99..81b431dc9a9 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java @@ -59,10 +59,11 @@ * process it, and transforms the gRPC response into {@link HttpServletResponse}. An adapter can be * instantiated by {@link ServletServerBuilder#buildServletAdapter()}. * - *
In a servlet, calling {@link #doPost(HttpServletRequest, HttpServletResponse)} inside {@link - * javax.servlet.http.HttpServlet#doPost(HttpServletRequest, HttpServletResponse)} makes the servlet - * backed by the gRPC server associated with the adapter. The servlet must support Asynchronous - * Processing and must be deployed to a container that supports servlet 4.0 and enables HTTP/2. + *
In a servlet, calling {@link #doPost(String, HttpServletRequest, HttpServletResponse)} inside + * {@link javax.servlet.http.HttpServlet#doPost(HttpServletRequest, HttpServletResponse)} makes + * the servlet backed by the gRPC server associated with the adapter. The servlet must support + * Asynchronous Processing and must be deployed to a container that supports servlet 4.0 + * and enables HTTP/2. * *
The API is experimental. The authors would like to know more about the real usecases. Users * are welcome to provide feedback by commenting on @@ -103,6 +104,10 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOExc resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "GET method not supported"); } + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + doPost(req.getRequestURI().substring(1), req, resp); + } + /** * Call this method inside {@link javax.servlet.http.HttpServlet#doPost(HttpServletRequest, * HttpServletResponse)} to serve gRPC POST request. @@ -110,7 +115,8 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOExc *
Do not modify {@code req} and {@code resp} before or after calling this method. However, * calling {@code resp.setBufferSize()} before invocation is allowed. */ - public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + public void doPost(String method, HttpServletRequest req, HttpServletResponse resp) + throws IOException { checkArgument(req.isAsyncSupported(), "servlet does not support asynchronous operation"); checkArgument(ServletAdapter.isGrpc(req), "the request is not a gRPC request"); @@ -119,7 +125,6 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOEx AsyncContext asyncCtx = req.startAsync(req, resp); - String method = req.getRequestURI().substring(1); // remove the leading "/" Metadata headers = getHeaders(req); if (logger.isLoggable(FINEST)) { diff --git a/servlet/src/test/java/io/grpc/servlet/GrpcServletTest.java b/servlet/src/test/java/io/grpc/servlet/GrpcServletTest.java new file mode 100644 index 00000000000..d471f92a266 --- /dev/null +++ b/servlet/src/test/java/io/grpc/servlet/GrpcServletTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2025 The gRPC Authors + * + * 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 io.grpc.servlet; + +import static io.grpc.servlet.GrpcServlet.REMOVE_CONTEXT_PATH; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GrpcServletTest { + private static final String EXPECTED_METHOD = "hello/world"; + private static final String CONTEXT_PATH = "/grpc"; + private static final String REQUEST_URI = "/hello/world"; + + @Test + public void defaultMethodTest() throws ServletException { + HttpServletRequest request = mock(HttpServletRequest.class); + GrpcServlet grpcServlet = mock(GrpcServlet.class); + + doCallRealMethod().when(grpcServlet).init(); + grpcServlet.init(); + + doReturn(REQUEST_URI).when(request).getRequestURI(); + when(grpcServlet.getMethod(request)).thenCallRealMethod(); + + assertEquals(EXPECTED_METHOD, grpcServlet.getMethod(request)); + } + + @Test + public void removeContextPathMethodTest() throws ServletException { + HttpServletRequest request = mock(HttpServletRequest.class); + GrpcServlet grpcServlet = mock(GrpcServlet.class); + + doReturn("true").when(grpcServlet).getInitParameter(REMOVE_CONTEXT_PATH); + doCallRealMethod().when(grpcServlet).init(); + grpcServlet.init(); + + doReturn(CONTEXT_PATH + REQUEST_URI).when(request).getRequestURI(); + doReturn(CONTEXT_PATH).when(request).getContextPath(); + when(grpcServlet.getMethod(request)).thenCallRealMethod(); + + assertEquals(EXPECTED_METHOD, grpcServlet.getMethod(request)); + } +}