• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #19991

19 Sep 2025 08:23PM UTC coverage: 88.54% (-0.007%) from 88.547%
#19991

push

github

ejona86
examples: Explain Bazel BCR releases and git_override option

34665 of 39152 relevant lines covered (88.54%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.03
/../servlet/src/main/java/io/grpc/servlet/ServletAdapter.java
1
/*
2
 * Copyright 2018 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.servlet;
18

19
import static com.google.common.base.Preconditions.checkArgument;
20
import static com.google.common.base.Preconditions.checkNotNull;
21
import static io.grpc.internal.GrpcUtil.TIMEOUT_KEY;
22
import static java.util.logging.Level.FINE;
23
import static java.util.logging.Level.FINEST;
24

25
import com.google.common.annotations.VisibleForTesting;
26
import com.google.common.io.BaseEncoding;
27
import io.grpc.Attributes;
28
import io.grpc.ExperimentalApi;
29
import io.grpc.Grpc;
30
import io.grpc.InternalLogId;
31
import io.grpc.InternalMetadata;
32
import io.grpc.Metadata;
33
import io.grpc.ServerStreamTracer;
34
import io.grpc.Status;
35
import io.grpc.internal.GrpcUtil;
36
import io.grpc.internal.ReadableBuffers;
37
import io.grpc.internal.ServerTransportListener;
38
import io.grpc.internal.StatsTraceContext;
39
import java.io.IOException;
40
import java.net.InetSocketAddress;
41
import java.net.URI;
42
import java.net.URISyntaxException;
43
import java.nio.charset.StandardCharsets;
44
import java.util.ArrayList;
45
import java.util.Arrays;
46
import java.util.Enumeration;
47
import java.util.List;
48
import java.util.concurrent.TimeUnit;
49
import java.util.function.Function;
50
import java.util.logging.Logger;
51
import javax.servlet.AsyncContext;
52
import javax.servlet.AsyncEvent;
53
import javax.servlet.AsyncListener;
54
import javax.servlet.ReadListener;
55
import javax.servlet.ServletInputStream;
56
import javax.servlet.http.HttpServletRequest;
57
import javax.servlet.http.HttpServletResponse;
58

59
/**
60
 * An adapter that transforms {@link HttpServletRequest} into gRPC request and lets a gRPC server
61
 * process it, and transforms the gRPC response into {@link HttpServletResponse}. An adapter can be
62
 * instantiated by {@link ServletServerBuilder#buildServletAdapter()}.
63
 *
64
 * <p>In a servlet, calling {@link #doPost(HttpServletRequest, HttpServletResponse)} inside {@link
65
 * javax.servlet.http.HttpServlet#doPost(HttpServletRequest, HttpServletResponse)} makes the servlet
66
 * backed by the gRPC server associated with the adapter. The servlet must support Asynchronous
67
 * Processing and must be deployed to a container that supports servlet 4.0 and enables HTTP/2.
68
 *
69
 * <p>The API is experimental. The authors would like to know more about the real usecases. Users
70
 * are welcome to provide feedback by commenting on
71
 * <a href=https://github.com/grpc/grpc-java/issues/5066>the tracking issue</a>.
72
 */
73
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/5066")
74
public final class ServletAdapter {
75

76
  static final Logger logger = Logger.getLogger(ServletAdapter.class.getName());
1✔
77
  static final Function<HttpServletRequest, String> DEFAULT_METHOD_NAME_RESOLVER =
1✔
78
          req -> req.getRequestURI().substring(1); // remove the leading "/"
1✔
79

80
  private final ServerTransportListener transportListener;
81
  private final List<? extends ServerStreamTracer.Factory> streamTracerFactories;
82
  private final Function<HttpServletRequest, String> methodNameResolver;
83
  private final int maxInboundMessageSize;
84
  private final Attributes attributes;
85

86
  ServletAdapter(
87
      ServerTransportListener transportListener,
88
      List<? extends ServerStreamTracer.Factory> streamTracerFactories,
89
      Function<HttpServletRequest, String> methodNameResolver,
90
      int maxInboundMessageSize) {
1✔
91
    this.transportListener = transportListener;
1✔
92
    this.streamTracerFactories = streamTracerFactories;
1✔
93
    this.methodNameResolver = methodNameResolver;
1✔
94
    this.maxInboundMessageSize = maxInboundMessageSize;
1✔
95
    attributes = transportListener.transportReady(Attributes.EMPTY);
1✔
96
  }
1✔
97

98
  /**
99
   * Call this method inside {@link javax.servlet.http.HttpServlet#doGet(HttpServletRequest,
100
   * HttpServletResponse)} to serve gRPC GET request.
101
   *
102
   * <p>This method is currently not implemented.
103
   *
104
   * <p>Note that in rare case gRPC client sends GET requests.
105
   *
106
   * <p>Do not modify {@code req} and {@code resp} before or after calling this method. However,
107
   * calling {@code resp.setBufferSize()} before invocation is allowed.
108
   */
109
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
110
    resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "GET method not supported");
×
111
  }
×
112

113
  /**
114
   * Call this method inside {@link javax.servlet.http.HttpServlet#doPost(HttpServletRequest,
115
   * HttpServletResponse)} to serve gRPC POST request.
116
   *
117
   * <p>Do not modify {@code req} and {@code resp} before or after calling this method. However,
118
   * calling {@code resp.setBufferSize()} before invocation is allowed.
119
   */
120
  public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
121
    checkArgument(req.isAsyncSupported(), "servlet does not support asynchronous operation");
1✔
122
    checkArgument(ServletAdapter.isGrpc(req), "the request is not a gRPC request");
1✔
123

124
    InternalLogId logId = InternalLogId.allocate(ServletAdapter.class, null);
1✔
125
    logger.log(FINE, "[{0}] RPC started", logId);
1✔
126

127
    AsyncContext asyncCtx = req.startAsync(req, resp);
1✔
128

129
    String method = methodNameResolver.apply(req);
1✔
130
    Metadata headers = getHeaders(req);
1✔
131

132
    if (logger.isLoggable(FINEST)) {
1✔
133
      logger.log(FINEST, "[{0}] method: {1}", new Object[] {logId, method});
×
134
      logger.log(FINEST, "[{0}] headers: {1}", new Object[] {logId, headers});
×
135
    }
136

137
    Long timeoutNanos = headers.get(TIMEOUT_KEY);
1✔
138
    asyncCtx.setTimeout(timeoutNanos != null
1✔
139
        ? TimeUnit.NANOSECONDS.toMillis(timeoutNanos) + ASYNC_TIMEOUT_SAFETY_MARGIN
1✔
140
        : 0);
1✔
141
    StatsTraceContext statsTraceCtx =
1✔
142
        StatsTraceContext.newServerContext(streamTracerFactories, method, headers);
1✔
143

144
    ServletServerStream stream = new ServletServerStream(
1✔
145
        asyncCtx,
146
        statsTraceCtx,
147
        maxInboundMessageSize,
148
        attributes.toBuilder()
1✔
149
            .set(
1✔
150
                Grpc.TRANSPORT_ATTR_REMOTE_ADDR,
151
                new InetSocketAddress(req.getRemoteHost(), req.getRemotePort()))
1✔
152
            .set(
1✔
153
                Grpc.TRANSPORT_ATTR_LOCAL_ADDR,
154
                new InetSocketAddress(req.getLocalAddr(), req.getLocalPort()))
1✔
155
            .build(),
1✔
156
        getAuthority(req),
1✔
157
        logId);
158

159
    transportListener.streamCreated(stream, method, headers);
1✔
160
    stream.transportState().runOnTransportThread(stream.transportState()::onStreamAllocated);
1✔
161

162
    asyncCtx.getRequest().getInputStream()
1✔
163
        .setReadListener(new GrpcReadListener(stream, asyncCtx, logId));
1✔
164
    asyncCtx.addListener(new GrpcAsyncListener(stream, logId));
1✔
165
  }
1✔
166

167
  /**
168
   * Deadlines are managed via Context, servlet async timeout is not supposed to happen.
169
   */
170
  @VisibleForTesting
171
  static final long ASYNC_TIMEOUT_SAFETY_MARGIN = 5_000;
172

173
  // This method must use Enumeration and its members, since that is the only way to read headers
174
  // from the servlet api.
175
  @SuppressWarnings("JdkObsolete")
176
  private static Metadata getHeaders(HttpServletRequest req) {
177
    Enumeration<String> headerNames = req.getHeaderNames();
1✔
178
    checkNotNull(
1✔
179
        headerNames, "Servlet container does not allow HttpServletRequest.getHeaderNames()");
180
    List<byte[]> byteArrays = new ArrayList<>();
1✔
181
    while (headerNames.hasMoreElements()) {
1✔
182
      String headerName = headerNames.nextElement();
1✔
183
      Enumeration<String> values = req.getHeaders(headerName);
1✔
184
      if (values == null) {
1✔
185
        continue;
×
186
      }
187
      while (values.hasMoreElements()) {
1✔
188
        String value = values.nextElement();
1✔
189
        if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
190
          byteArrays.add(headerName.getBytes(StandardCharsets.US_ASCII));
1✔
191
          byteArrays.add(BaseEncoding.base64().decode(value));
1✔
192
        } else {
193
          byteArrays.add(headerName.getBytes(StandardCharsets.US_ASCII));
1✔
194
          byteArrays.add(value.getBytes(StandardCharsets.US_ASCII));
1✔
195
        }
196
      }
1✔
197
    }
1✔
198
    return InternalMetadata.newMetadata(byteArrays.toArray(new byte[][]{}));
1✔
199
  }
200

201
  // This method must use HttpRequest#getRequestURL or HttpUtils#getRequestURL, both of which
202
  // can only return StringBuffer instances
203
  @SuppressWarnings("JdkObsolete")
204
  private static String getAuthority(HttpServletRequest req) {
205
    try {
206
      return new URI(req.getRequestURL().toString()).getAuthority();
1✔
207
    } catch (URISyntaxException e) {
×
208
      logger.log(FINE, "Error getting authority from the request URL {0}", req.getRequestURL());
×
209
      return req.getServerName() + ":" + req.getServerPort();
×
210
    }
211
  }
212

213
  /**
214
   * Call this method when the adapter is no longer needed. The gRPC server will be terminated.
215
   */
216
  public void destroy() {
217
    transportListener.transportTerminated();
1✔
218
  }
1✔
219

220
  private static final class GrpcAsyncListener implements AsyncListener {
221
    final InternalLogId logId;
222
    final ServletServerStream stream;
223

224
    GrpcAsyncListener(ServletServerStream stream, InternalLogId logId) {
1✔
225
      this.stream = stream;
1✔
226
      this.logId = logId;
1✔
227
    }
1✔
228

229
    @Override
230
    public void onComplete(AsyncEvent event) {
231
      stream.asyncCompleted = true;
1✔
232
    }
1✔
233

234
    @Override
235
    public void onTimeout(AsyncEvent event) {
236
      if (logger.isLoggable(FINE)) {
1✔
237
        logger.log(FINE, String.format("[{%s}] Timeout: ", logId), event.getThrowable());
×
238
      }
239
      // If the resp is not committed, cancel() to avoid being redirected to an error page.
240
      // Else, the container will send RST_STREAM in the end.
241
      if (!event.getAsyncContext().getResponse().isCommitted()) {
1✔
242
        stream.cancel(Status.DEADLINE_EXCEEDED);
1✔
243
      } else {
244
        stream.transportState().runOnTransportThread(
1✔
245
            () -> stream.transportState().transportReportStatus(Status.DEADLINE_EXCEEDED));
1✔
246
      }
247
    }
1✔
248

249
    @Override
250
    public void onError(AsyncEvent event) {
251
      if (logger.isLoggable(FINE)) {
×
252
        logger.log(FINE, String.format("[{%s}] Error: ", logId), event.getThrowable());
×
253
      }
254

255
      // If the resp is not committed, cancel() to avoid being redirected to an error page.
256
      // Else, the container will send RST_STREAM at the end.
257
      if (!event.getAsyncContext().getResponse().isCommitted()) {
×
258
        stream.cancel(Status.fromThrowable(event.getThrowable()));
×
259
      } else {
260
        stream.transportState().runOnTransportThread(
×
261
            () -> stream.transportState().transportReportStatus(
×
262
                Status.fromThrowable(event.getThrowable())));
×
263
      }
264
    }
×
265

266
    @Override
267
    public void onStartAsync(AsyncEvent event) {}
×
268
  }
269

270
  private static final class GrpcReadListener implements ReadListener {
271
    final ServletServerStream stream;
272
    final AsyncContext asyncCtx;
273
    final ServletInputStream input;
274
    final InternalLogId logId;
275

276
    GrpcReadListener(
277
        ServletServerStream stream,
278
        AsyncContext asyncCtx,
279
        InternalLogId logId) throws IOException {
1✔
280
      this.stream = stream;
1✔
281
      this.asyncCtx = asyncCtx;
1✔
282
      input = asyncCtx.getRequest().getInputStream();
1✔
283
      this.logId = logId;
1✔
284
    }
1✔
285

286
    final byte[] buffer = new byte[4 * 1024];
1✔
287

288
    @Override
289
    public void onDataAvailable() throws IOException {
290
      logger.log(FINEST, "[{0}] onDataAvailable: ENTRY", logId);
1✔
291

292
      while (input.isReady()) {
1✔
293
        int length = input.read(buffer);
1✔
294
        if (length == -1) {
1✔
295
          logger.log(FINEST, "[{0}] inbound data: read end of stream", logId);
×
296
          return;
×
297
        } else {
298
          if (logger.isLoggable(FINEST)) {
1✔
299
            logger.log(
×
300
                FINEST,
301
                "[{0}] inbound data: length = {1}, bytes = {2}",
302
                new Object[] {logId, length, ServletServerStream.toHexString(buffer, length)});
×
303
          }
304

305
          byte[] copy = Arrays.copyOf(buffer, length);
1✔
306
          stream.transportState().runOnTransportThread(
1✔
307
              () -> stream.transportState().inboundDataReceived(ReadableBuffers.wrap(copy), false));
1✔
308
        }
309
      }
1✔
310

311
      logger.log(FINEST, "[{0}] onDataAvailable: EXIT", logId);
1✔
312
    }
1✔
313

314
    @Override
315
    public void onAllDataRead() {
316
      logger.log(FINE, "[{0}] onAllDataRead", logId);
1✔
317
      stream.transportState().runOnTransportThread(() ->
1✔
318
          stream.transportState().inboundDataReceived(ReadableBuffers.empty(), true));
1✔
319
    }
1✔
320

321
    @Override
322
    public void onError(Throwable t) {
323
      if (logger.isLoggable(FINE)) {
1✔
324
        logger.log(FINE, String.format("[{%s}] Error: ", logId), t);
×
325
      }
326
      // If the resp is not committed, cancel() to avoid being redirected to an error page.
327
      // Else, the container will send RST_STREAM at the end.
328
      if (!asyncCtx.getResponse().isCommitted()) {
1✔
329
        stream.cancel(Status.fromThrowable(t));
1✔
330
      } else {
331
        stream.transportState().runOnTransportThread(
×
332
            () -> stream.transportState()
×
333
                .transportReportStatus(Status.fromThrowable(t)));
×
334
      }
335
    }
1✔
336
  }
337

338
  /**
339
   * Checks whether an incoming {@code HttpServletRequest} may come from a gRPC client.
340
   *
341
   * @return true if the request comes from a gRPC client
342
   */
343
  public static boolean isGrpc(HttpServletRequest request) {
344
    return request.getContentType() != null
1✔
345
        && request.getContentType().contains(GrpcUtil.CONTENT_TYPE_GRPC);
1✔
346
  }
347
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc