diff --git a/.github/workflows/actions_build.yml b/.github/workflows/actions_build.yml index f072a34d976..42b933305e0 100644 --- a/.github/workflows/actions_build.yml +++ b/.github/workflows/actions_build.yml @@ -2,7 +2,7 @@ name: CI on: push: branches: - - main + - "**" tags-ignore: # The release versions will be verified by 'publish-release.yml' - armeria-* diff --git a/annotation-processor/src/main/java/com/linecorp/armeria/server/annotation/processor/DocumentationProcessor.java b/annotation-processor/src/main/java/com/linecorp/armeria/server/annotation/processor/DocumentationProcessor.java index 9530a6a6139..6f9b9387f78 100644 --- a/annotation-processor/src/main/java/com/linecorp/armeria/server/annotation/processor/DocumentationProcessor.java +++ b/annotation-processor/src/main/java/com/linecorp/armeria/server/annotation/processor/DocumentationProcessor.java @@ -38,6 +38,7 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.QualifiedNameable; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.tools.Diagnostic.Kind; @@ -148,7 +149,9 @@ private void processAnnotation(TypeElement annotationElement, RoundEnvironment r } private void processMethod(ExecutableElement method) throws IOException { - final String className = ((TypeElement) method.getEnclosingElement()).getQualifiedName().toString(); + final QualifiedNameable enclosingElement = (QualifiedNameable) method.getEnclosingElement(); + assert enclosingElement != null; + final String className = enclosingElement.getQualifiedName().toString(); final Properties properties = readProperties(className); final String docComment = processingEnv.getElementUtils().getDocComment(method); if (docComment == null || !docComment.contains("@param")) { diff --git a/benchmarks/jmh/src/jmh/java/com/linecorp/armeria/server/RoutersBenchmark.java b/benchmarks/jmh/src/jmh/java/com/linecorp/armeria/server/RoutersBenchmark.java index 9fc9badb900..f91d2226685 100644 --- a/benchmarks/jmh/src/jmh/java/com/linecorp/armeria/server/RoutersBenchmark.java +++ b/benchmarks/jmh/src/jmh/java/com/linecorp/armeria/server/RoutersBenchmark.java @@ -78,7 +78,7 @@ private static ServiceConfig newServiceConfig(Route route) { final Path multipartUploadsLocation = Flags.defaultMultipartUploadsLocation(); final ServiceErrorHandler serviceErrorHandler = ServerErrorHandler.ofDefault().asServiceErrorHandler(); return new ServiceConfig(route, route, - SERVICE, defaultLogName, defaultServiceName, defaultServiceNaming, 0, 0, + SERVICE, defaultServiceName, defaultServiceNaming, defaultLogName, 0, 0, false, AccessLogWriter.disabled(), CommonPools.blockingTaskExecutor(), SuccessFunction.always(), 0, multipartUploadsLocation, MultipartRemovalStrategy.ON_RESPONSE_COMPLETION, diff --git a/brave/brave6/src/main/java/com/linecorp/armeria/common/brave/RequestContextCurrentTraceContext.java b/brave/brave6/src/main/java/com/linecorp/armeria/common/brave/RequestContextCurrentTraceContext.java index 429a698ebca..7029bb413a4 100644 --- a/brave/brave6/src/main/java/com/linecorp/armeria/common/brave/RequestContextCurrentTraceContext.java +++ b/brave/brave6/src/main/java/com/linecorp/armeria/common/brave/RequestContextCurrentTraceContext.java @@ -174,7 +174,7 @@ public Scope newScope(@Nullable TraceContext currentSpan) { @UnstableApi @Override - public Scope decorateScope(TraceContext context, Scope scope) { + public Scope decorateScope(@Nullable TraceContext context, Scope scope) { // If a `Scope` is decorated, `ScopeDecorator`s populate some contexts as such as MDC, which are stored // to a thread-local. The activated contexts will be removed when `decoratedScope.close()` is called. // If `Scope.NOOP` is specified, CurrentTraceContext.decorateScope() performs nothing. diff --git a/brave/brave6/src/main/java/com/linecorp/armeria/internal/common/brave/TraceContextUtil.java b/brave/brave6/src/main/java/com/linecorp/armeria/internal/common/brave/TraceContextUtil.java index 67f94a5fc62..ad3f5890b52 100644 --- a/brave/brave6/src/main/java/com/linecorp/armeria/internal/common/brave/TraceContextUtil.java +++ b/brave/brave6/src/main/java/com/linecorp/armeria/internal/common/brave/TraceContextUtil.java @@ -39,7 +39,7 @@ public static TraceContext traceContext(RequestContext ctx) { return ctx.attr(TRACE_CONTEXT_KEY); } - public static void setTraceContext(RequestContext ctx, TraceContext traceContext) { + public static void setTraceContext(RequestContext ctx, @Nullable TraceContext traceContext) { ctx.setAttr(TRACE_CONTEXT_KEY, traceContext); } diff --git a/build.gradle b/build.gradle index 54f6f53290d..2a28be232be 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ plugins { alias libs.plugins.kotlin apply false alias libs.plugins.ktlint apply false alias libs.plugins.errorprone apply false + alias libs.plugins.nullaway apply false } allprojects { @@ -171,13 +172,31 @@ configure(projectsWithFlags('java')) { // Error Prone compiler if (!rootProject.hasProperty('noLint')) { apply plugin: 'net.ltgt.errorprone' + apply plugin: 'net.ltgt.nullaway' dependencies { errorprone libs.errorprone.core + errorprone libs.nullaway + } + + nullaway { + annotatedPackages.add("com.linecorp.armeria") } tasks.withType(JavaCompile) { options.errorprone.excludedPaths = '.*/gen-src/.*' + options.errorprone.nullaway { + if (name.contains("Test") || name.contains("Jmh")) { + // Disable NullAway for tests and benchmarks for now. + disable() + } else if (name.matches(/compileJava[0-9]+.*/)) { + // Disable MR-JAR classes which seem to confuse NullAway and break the build. + disable() + } else { + error() + assertsEnabled = true + } + } } } diff --git a/consul/src/main/java/com/linecorp/armeria/internal/consul/HealthClient.java b/consul/src/main/java/com/linecorp/armeria/internal/consul/HealthClient.java index 1b6ec8d3594..a650643983b 100644 --- a/consul/src/main/java/com/linecorp/armeria/internal/consul/HealthClient.java +++ b/consul/src/main/java/com/linecorp/armeria/internal/consul/HealthClient.java @@ -110,6 +110,16 @@ CompletableFuture> healthyEndpoints(String serviceName, @Nullable @Nullable private static Endpoint toEndpoint(HealthService healthService) { + if (healthService.service == null) { + return null; + } + if (healthService.node == null) { + return null; + } + + assert healthService.service != null; + assert healthService.node != null; + if (!Strings.isNullOrEmpty(healthService.service.address)) { return Endpoint.of(healthService.service.address, healthService.service.port); } else if (!Strings.isNullOrEmpty(healthService.node.address)) { @@ -122,9 +132,11 @@ private static Endpoint toEndpoint(HealthService healthService) { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) private static final class HealthService { + @Nullable @JsonProperty("Node") Node node; + @Nullable @JsonProperty("Service") Service service; } @@ -132,21 +144,27 @@ private static final class HealthService { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) private static final class Node { + @Nullable @JsonProperty("ID") String id; + @Nullable @JsonProperty("Node") String node; + @Nullable @JsonProperty("Address") String address; + @Nullable @JsonProperty("Datacenter") String datacenter; + @Nullable @JsonProperty("TaggedAddresses") Object taggedAddresses; + @Nullable @JsonProperty("Meta") Map meta; } @@ -154,27 +172,34 @@ private static final class Node { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) public static final class Service { + @Nullable @JsonProperty("ID") String id; + @Nullable @JsonProperty("Service") String service; + @Nullable @JsonProperty("Tags") String[] tags; + @Nullable @JsonProperty("Address") String address; + @Nullable @JsonProperty("TaggedAddresses") Object taggedAddresses; + @Nullable @JsonProperty("Meta") Map meta; @JsonProperty("Port") int port; + @Nullable @JsonProperty("Weights") Map weights; } diff --git a/core/src/main/java/com/linecorp/armeria/client/AbstractHttpRequestHandler.java b/core/src/main/java/com/linecorp/armeria/client/AbstractHttpRequestHandler.java index a25a5b9fd6b..6ed0240c6b1 100644 --- a/core/src/main/java/com/linecorp/armeria/client/AbstractHttpRequestHandler.java +++ b/core/src/main/java/com/linecorp/armeria/client/AbstractHttpRequestHandler.java @@ -54,7 +54,6 @@ import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http2.Http2Error; -import io.netty.handler.proxy.ProxyConnectException; abstract class AbstractHttpRequestHandler implements ChannelFutureListener { @@ -216,6 +215,7 @@ RequestHeaders mergedRequestHeaders(RequestHeaders headers) { * {@link Channel#flush()} when each write unit is done. */ final void writeHeaders(RequestHeaders headers, boolean needs100Continue) { + assert session != null; final SessionProtocol protocol = session.protocol(); assert protocol != null; if (needs100Continue) { @@ -377,8 +377,7 @@ final void failAndReset(Throwable cause) { session.markUnacquirable(); } - if (cause instanceof ProxyConnectException || cause instanceof ResponseCompleteException) { - // - ProxyConnectException is handled by HttpSessionHandler.exceptionCaught(). + if (cause instanceof ResponseCompleteException) { // - ResponseCompleteException means the response is successfully received. state = State.DONE; cancel(); diff --git a/core/src/main/java/com/linecorp/armeria/client/Client.java b/core/src/main/java/com/linecorp/armeria/client/Client.java index 6b15ae76729..d5871efb848 100644 --- a/core/src/main/java/com/linecorp/armeria/client/Client.java +++ b/core/src/main/java/com/linecorp/armeria/client/Client.java @@ -24,6 +24,7 @@ import com.linecorp.armeria.common.Response; import com.linecorp.armeria.common.RpcRequest; import com.linecorp.armeria.common.RpcResponse; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.Unwrappable; /** @@ -71,6 +72,7 @@ public interface Client extends Unwrappab * @see ClientFactory#unwrap(Object, Class) * @see Unwrappable */ + @Nullable @Override default T as(Class type) { requireNonNull(type, "type"); diff --git a/core/src/main/java/com/linecorp/armeria/client/ClientRequestContext.java b/core/src/main/java/com/linecorp/armeria/client/ClientRequestContext.java index 25ecab100a1..4aea2c21933 100644 --- a/core/src/main/java/com/linecorp/armeria/client/ClientRequestContext.java +++ b/core/src/main/java/com/linecorp/armeria/client/ClientRequestContext.java @@ -42,6 +42,7 @@ import com.linecorp.armeria.common.RequestId; import com.linecorp.armeria.common.Response; import com.linecorp.armeria.common.RpcRequest; +import com.linecorp.armeria.common.TimeoutException; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; import com.linecorp.armeria.common.logging.RequestLog; @@ -514,6 +515,21 @@ default void timeoutNow() { cancel(ResponseTimeoutException.get()); } + /** + * Returns whether this {@link ClientRequestContext} has been timed-out, that is the cancellation cause + * is an instance of {@link TimeoutException} or + * {@link UnprocessedRequestException} and wrapped cause is {@link TimeoutException}. + */ + @Override + default boolean isTimedOut() { + if (RequestContext.super.isTimedOut()) { + return true; + } + final Throwable cause = cancellationCause(); + return cause instanceof TimeoutException || + cause instanceof UnprocessedRequestException && cause.getCause() instanceof TimeoutException; + } + /** * Returns the maximum length of the received {@link Response}. * This value is initially set from {@link ClientOptions#MAX_RESPONSE_LENGTH}. diff --git a/core/src/main/java/com/linecorp/armeria/client/ClientRequestContextWrapper.java b/core/src/main/java/com/linecorp/armeria/client/ClientRequestContextWrapper.java index 91f0c564648..6e411d99fb0 100644 --- a/core/src/main/java/com/linecorp/armeria/client/ClientRequestContextWrapper.java +++ b/core/src/main/java/com/linecorp/armeria/client/ClientRequestContextWrapper.java @@ -52,26 +52,31 @@ public ClientRequestContext newDerivedContext(RequestId id, @Nullable HttpReques return unwrap().newDerivedContext(id, req, rpcReq, endpoint); } + @Nullable @Override public EndpointGroup endpointGroup() { return unwrap().endpointGroup(); } + @Nullable @Override public Endpoint endpoint() { return unwrap().endpoint(); } + @Nullable @Override public String fragment() { return unwrap().fragment(); } + @Nullable @Override public String authority() { return unwrap().authority(); } + @Nullable @Override public String host() { return unwrap().host(); diff --git a/core/src/main/java/com/linecorp/armeria/client/DecoratingClientFactory.java b/core/src/main/java/com/linecorp/armeria/client/DecoratingClientFactory.java index e8d3bd46ac1..5b083dc1da8 100644 --- a/core/src/main/java/com/linecorp/armeria/client/DecoratingClientFactory.java +++ b/core/src/main/java/com/linecorp/armeria/client/DecoratingClientFactory.java @@ -108,11 +108,13 @@ public Object newClient(ClientBuilderParams params) { return unwrap().newClient(params); } + @Nullable @Override public ClientBuilderParams clientBuilderParams(T client) { return unwrap().clientBuilderParams(client); } + @Nullable @Override public T unwrap(Object client, Class type) { return unwrap().unwrap(client, type); diff --git a/core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java b/core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java index d91080000f3..e8b82251882 100644 --- a/core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java +++ b/core/src/main/java/com/linecorp/armeria/client/DefaultClientFactory.java @@ -184,6 +184,7 @@ public Object newClient(ClientBuilderParams params) { "No ClientFactory for scheme: " + scheme + " matched clientType: " + clientType); } + @Nullable @Override public T unwrap(Object client, Class type) { final T params = ClientFactory.super.unwrap(client, type); diff --git a/core/src/main/java/com/linecorp/armeria/client/DefaultDnsCache.java b/core/src/main/java/com/linecorp/armeria/client/DefaultDnsCache.java index a0fabdab3e5..ab4634d2197 100644 --- a/core/src/main/java/com/linecorp/armeria/client/DefaultDnsCache.java +++ b/core/src/main/java/com/linecorp/armeria/client/DefaultDnsCache.java @@ -147,6 +147,7 @@ public void cache(DnsQuestion question, UnknownHostException cause) { } } + @Nullable @Override public List get(DnsQuestion question) throws UnknownHostException { requireNonNull(question, "question"); diff --git a/core/src/main/java/com/linecorp/armeria/client/DefaultRequestOptions.java b/core/src/main/java/com/linecorp/armeria/client/DefaultRequestOptions.java index 17d460c58bb..0a649c251cf 100644 --- a/core/src/main/java/com/linecorp/armeria/client/DefaultRequestOptions.java +++ b/core/src/main/java/com/linecorp/armeria/client/DefaultRequestOptions.java @@ -68,6 +68,7 @@ public long maxResponseLength() { return maxResponseLength; } + @Nullable @Override public Long requestAutoAbortDelayMillis() { return requestAutoAbortDelayMillis; @@ -78,6 +79,7 @@ public Map, Object> attrs() { return attributeMap; } + @Nullable @Override public ExchangeType exchangeType() { return exchangeType; diff --git a/core/src/main/java/com/linecorp/armeria/client/Endpoint.java b/core/src/main/java/com/linecorp/armeria/client/Endpoint.java index 7077116a171..500e8622ab9 100644 --- a/core/src/main/java/com/linecorp/armeria/client/Endpoint.java +++ b/core/src/main/java/com/linecorp/armeria/client/Endpoint.java @@ -37,6 +37,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer; +import javax.annotation.Nonnull; + import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.annotations.VisibleForTesting; @@ -323,6 +325,7 @@ public EndpointSelectionStrategy selectionStrategy() { return EndpointSelectionStrategy.weightedRoundRobin(); } + @Nonnull @Override public Endpoint selectNow(ClientRequestContext ctx) { return this; @@ -693,7 +696,7 @@ public Endpoint withAttr(AttributeKey key, @Nullable T value) { if (value == null) { return this; } - return withAttrs(Attributes.of(key, value)); + return replaceAttrs(Attributes.of(key, value)); } if (attributes.attr(key) == value) { @@ -701,8 +704,32 @@ public Endpoint withAttr(AttributeKey key, @Nullable T value) { } else { final AttributesBuilder attributesBuilder = attributes.toBuilder(); attributesBuilder.set(key, value); - return withAttrs(attributesBuilder.build()); + return replaceAttrs(attributesBuilder.build()); + } + } + + /** + * Returns a new {@link Endpoint} with the specified {@link Attributes}. + * Note that the {@link #attrs()} of this {@link Endpoint} is merged with the specified + * {@link Attributes}. For attributes with the same {@link AttributeKey}, the attribute + * in {@param newAttributes} has higher precedence. + */ + @UnstableApi + @SuppressWarnings("unchecked") + public Endpoint withAttrs(Attributes newAttributes) { + requireNonNull(newAttributes, "newAttributes"); + if (newAttributes.isEmpty()) { + return this; } + if (attrs().isEmpty()) { + return replaceAttrs(newAttributes); + } + final AttributesBuilder builder = attrs().toBuilder(); + newAttributes.attrs().forEachRemaining(entry -> { + final AttributeKey key = (AttributeKey) entry.getKey(); + builder.set(key, entry.getValue()); + }); + return new Endpoint(type, host, ipAddr, port, weight, builder.build()); } /** @@ -711,7 +738,7 @@ public Endpoint withAttr(AttributeKey key, @Nullable T value) { * {@link Attributes}. */ @UnstableApi - public Endpoint withAttrs(Attributes newAttributes) { + public Endpoint replaceAttrs(Attributes newAttributes) { requireNonNull(newAttributes, "newAttributes"); if (attrs().isEmpty() && newAttributes.isEmpty()) { return this; diff --git a/core/src/main/java/com/linecorp/armeria/client/FutureTransformingRequestPreparation.java b/core/src/main/java/com/linecorp/armeria/client/FutureTransformingRequestPreparation.java index 633ee0b4180..b2bc3e9bca2 100644 --- a/core/src/main/java/com/linecorp/armeria/client/FutureTransformingRequestPreparation.java +++ b/core/src/main/java/com/linecorp/armeria/client/FutureTransformingRequestPreparation.java @@ -88,6 +88,7 @@ public CompletableFuture execute() { return response.exceptionally(cause -> { cause = Exceptions.peel(cause); + assert errorHandler != null; final Object maybeRecovered = errorHandler.apply(cause); if (maybeRecovered instanceof Throwable) { // The cause was translated. diff --git a/core/src/main/java/com/linecorp/armeria/client/HAProxyHandler.java b/core/src/main/java/com/linecorp/armeria/client/HAProxyHandler.java index a50dc5d3596..f9fbae6efd5 100644 --- a/core/src/main/java/com/linecorp/armeria/client/HAProxyHandler.java +++ b/core/src/main/java/com/linecorp/armeria/client/HAProxyHandler.java @@ -60,8 +60,12 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception { final InetSocketAddress proxyAddress = haProxyConfig.proxyAddress(); assert proxyAddress != null; - promise.addListener(f -> { + final ChannelPromise connectionPromise = ctx.newPromise(); + ctx.connect(proxyAddress, localAddress, connectionPromise); + connectionPromise.addListener(f -> { if (!f.isSuccess()) { + promise.tryFailure(wrapException(f.cause())); + ctx.close(); return; } try { @@ -71,20 +75,20 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, ctx.pipeline().remove(HAProxyMessageEncoder.INSTANCE); final ProxyConnectionEvent event = new ProxyConnectionEvent( PROTOCOL, AUTH, proxyAddress, remoteAddress); + promise.trySuccess(); ctx.pipeline().fireUserEventTriggered(event); } else { - ctx.fireExceptionCaught(wrapException(f0.cause())); + promise.tryFailure(wrapException(f0.cause())); ctx.close(); } }); } catch (Exception e) { - ctx.pipeline().fireUserEventTriggered(wrapException(e)); + promise.tryFailure(wrapException(e)); ctx.close(); } finally { ctx.pipeline().remove(this); } }); - super.connect(ctx, proxyAddress, localAddress, promise); } private static ProxyConnectException wrapException(Throwable e) { @@ -123,4 +127,3 @@ private static Inet6Address translateToInet6(InetAddress inetAddress) { return NetUtil.getByName(inetAddress.getHostAddress()); } } - diff --git a/core/src/main/java/com/linecorp/armeria/client/HttpChannelPool.java b/core/src/main/java/com/linecorp/armeria/client/HttpChannelPool.java index a58e402e6c4..57e9fa13261 100644 --- a/core/src/main/java/com/linecorp/armeria/client/HttpChannelPool.java +++ b/core/src/main/java/com/linecorp/armeria/client/HttpChannelPool.java @@ -110,9 +110,12 @@ final class HttpChannelPool implements AsyncCloseable { SessionProtocol.H2, SessionProtocol.H2C)); pendingAcquisitions = newEnumMap(httpAndHttpsValues()); allChannels = new IdentityHashMap<>(); - connectTimeoutMillis = (Integer) clientFactory.options() - .channelOptions() - .get(ChannelOption.CONNECT_TIMEOUT_MILLIS); + final Integer connectTimeoutMillisBoxed = + (Integer) clientFactory.options() + .channelOptions() + .get(ChannelOption.CONNECT_TIMEOUT_MILLIS); + assert connectTimeoutMillisBoxed != null; + connectTimeoutMillis = connectTimeoutMillisBoxed; bootstraps = new Bootstraps(clientFactory, eventLoop, sslCtxHttp1Or2, sslCtxHttp1Only); } @@ -828,6 +831,7 @@ private void handlePiggyback(SessionProtocol desiredProtocol, switch (result) { case SUCCESS: + assert pch != null; timingsBuilder.pendingAcquisitionEnd(); childPromise.complete(pch); break; diff --git a/core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java b/core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java index e54959ac233..db7b7f5dc57 100644 --- a/core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java +++ b/core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java @@ -244,6 +244,7 @@ private static void handleEarlyRequestException(ClientRequestContext ctx, final RequestLogBuilder logBuilder = ctx.logBuilder(); logBuilder.endRequest(cause); logBuilder.endResponse(cause); + ctx.cancel(cause); } } diff --git a/core/src/main/java/com/linecorp/armeria/client/HttpClientPipelineConfigurator.java b/core/src/main/java/com/linecorp/armeria/client/HttpClientPipelineConfigurator.java index 1119c68b35b..4d742e06a24 100644 --- a/core/src/main/java/com/linecorp/armeria/client/HttpClientPipelineConfigurator.java +++ b/core/src/main/java/com/linecorp/armeria/client/HttpClientPipelineConfigurator.java @@ -132,6 +132,14 @@ final class HttpClientPipelineConfigurator extends ChannelDuplexHandler { .responseTimeoutMillis(0) .maxResponseLength(UPGRADE_RESPONSE_MAX_LENGTH).build(); + private static final RequestTarget REQ_TARGET_ASTERISK; + + static { + final RequestTarget asterisk = RequestTarget.forClient("*"); + assert asterisk != null; + REQ_TARGET_ASTERISK = asterisk; + } + private enum HttpPreference { HTTP1_REQUIRED, HTTP2_PREFERRED, @@ -215,7 +223,7 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, Sock * See HTTP/2 specification. */ private void configureAsHttps(Channel ch, SocketAddress remoteAddr) { - assert isHttps(); + assert sslCtx != null; final ChannelPipeline p = ch.pipeline(); final SSLEngine sslEngine; @@ -564,7 +572,7 @@ public void onComplete() {} final DefaultClientRequestContext reqCtx = new DefaultClientRequestContext( ctx.channel().eventLoop(), Flags.meterRegistry(), H1C, RequestId.random(), com.linecorp.armeria.common.HttpMethod.OPTIONS, - RequestTarget.forClient("*"), ClientOptions.of(), + REQ_TARGET_ASTERISK, ClientOptions.of(), HttpRequest.of(com.linecorp.armeria.common.HttpMethod.OPTIONS, "*"), null, REQUEST_OPTIONS_FOR_UPGRADE_REQUEST, CancellationScheduler.noop(), System.nanoTime(), SystemInfo.currentTimeMicros()); diff --git a/core/src/main/java/com/linecorp/armeria/client/HttpResponseWrapper.java b/core/src/main/java/com/linecorp/armeria/client/HttpResponseWrapper.java index d9adc43ac95..a40972b5746 100644 --- a/core/src/main/java/com/linecorp/armeria/client/HttpResponseWrapper.java +++ b/core/src/main/java/com/linecorp/armeria/client/HttpResponseWrapper.java @@ -284,7 +284,9 @@ private void cancelTimeoutOrLog(@Nullable Throwable cause, boolean cancel) { } final StringBuilder logMsg = new StringBuilder("Unexpected exception while closing a request"); - final String authority = ctx.request().authority(); + final HttpRequest request = ctx.request(); + assert request != null; + final String authority = request.authority(); if (authority != null) { logMsg.append(" to ").append(authority); } @@ -311,7 +313,9 @@ public boolean canSchedule() { @Override public void run(Throwable cause) { delegate.close(cause); - ctx.request().abort(cause); + final HttpRequest request = ctx.request(); + assert request != null; + request.abort(cause); ctx.logBuilder().endResponse(cause); } }; diff --git a/core/src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java b/core/src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java index 01375220af8..aea688bb418 100644 --- a/core/src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java +++ b/core/src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java @@ -166,6 +166,7 @@ public SerializationFormat serializationFormat() { return serializationFormat; } + @Nullable @Override public SessionProtocol protocol() { return protocol; @@ -223,8 +224,8 @@ public void invoke(PooledChannel pooledChannel, ClientRequestContext ctx, final long writeTimeoutMillis = ctx.writeTimeoutMillis(); assert protocol != null; - assert responseDecoder != null; assert requestEncoder != null; + assert responseDecoder != null; if (!protocol.isMultiplex() && !serializationFormat.requiresNewConnection(protocol)) { // When HTTP/1.1 is used and the serialization format does not require // a new connection (w.g. WebSocket): @@ -236,6 +237,7 @@ public void invoke(PooledChannel pooledChannel, ClientRequestContext ctx, useHttp1Pipelining ? req.whenComplete() : CompletableFuture.allOf(req.whenComplete(), res.whenComplete()); completionFuture.handle((ret, cause) -> { + assert responseDecoder != null; if (isAcquirable(responseDecoder.keepAliveHandler())) { pooledChannel.release(); } @@ -426,8 +428,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc return; } - if (evt instanceof SessionProtocolNegotiationException || - evt instanceof ProxyConnectException) { + if (evt instanceof SessionProtocolNegotiationException) { tryFailSessionPromise((Throwable) evt); ctx.close(); return; diff --git a/core/src/main/java/com/linecorp/armeria/client/NoopHostFileEntriesResolver.java b/core/src/main/java/com/linecorp/armeria/client/NoopHostFileEntriesResolver.java index c9303161a0d..8dc905697ca 100644 --- a/core/src/main/java/com/linecorp/armeria/client/NoopHostFileEntriesResolver.java +++ b/core/src/main/java/com/linecorp/armeria/client/NoopHostFileEntriesResolver.java @@ -18,12 +18,15 @@ import java.net.InetAddress; +import com.linecorp.armeria.common.annotation.Nullable; + import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.ResolvedAddressTypes; enum NoopHostFileEntriesResolver implements HostsFileEntriesResolver { INSTANCE; + @Nullable @Override public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) { return null; diff --git a/core/src/main/java/com/linecorp/armeria/client/RedirectingClient.java b/core/src/main/java/com/linecorp/armeria/client/RedirectingClient.java index 3ee0b13d5ac..d2481315513 100644 --- a/core/src/main/java/com/linecorp/armeria/client/RedirectingClient.java +++ b/core/src/main/java/com/linecorp/armeria/client/RedirectingClient.java @@ -561,15 +561,21 @@ void validateRedirects(RequestTarget nextReqTarget, HttpMethod nextMethod, int m redirectSignatures = new LinkedHashSet<>(); final String originalProtocol = ctx.sessionProtocol().isTls() ? "https" : "http"; + final String originalAuthority = ctx.authority(); + assert originalAuthority != null; final RedirectSignature originalSignature = new RedirectSignature(originalProtocol, - ctx.authority(), + originalAuthority, request.headers().path(), request.method()); redirectSignatures.add(originalSignature); } - final RedirectSignature signature = new RedirectSignature(nextReqTarget.scheme(), - nextReqTarget.authority(), + final String nextScheme = nextReqTarget.scheme(); + final String nextAuthority = nextReqTarget.authority(); + assert nextScheme != null; + assert nextAuthority != null; + final RedirectSignature signature = new RedirectSignature(nextScheme, + nextAuthority, nextReqTarget.pathAndQuery(), nextMethod); if (!redirectSignatures.add(signature)) { diff --git a/core/src/main/java/com/linecorp/armeria/client/RefreshingAddressResolver.java b/core/src/main/java/com/linecorp/armeria/client/RefreshingAddressResolver.java index 58efd01884d..3a2e6c61a6a 100644 --- a/core/src/main/java/com/linecorp/armeria/client/RefreshingAddressResolver.java +++ b/core/src/main/java/com/linecorp/armeria/client/RefreshingAddressResolver.java @@ -350,6 +350,7 @@ void refresh() { } refreshing = true; + assert address != null; final String hostname = address.getHostName(); // 'sendQueries()' always successfully completes. sendQueries(questions, hostname, originalCreationTimeNanos).thenAccept(entry -> { @@ -370,6 +371,7 @@ private Object maybeUpdate(String hostname, CacheEntry entry) { final Throwable cause = entry.cause(); if (cause != null) { + assert autoRefreshBackoff != null; final long nextDelayMillis = autoRefreshBackoff.nextDelayMillis(numAttemptsSoFar++); if (nextDelayMillis < 0) { @@ -401,6 +403,8 @@ boolean refreshable() { return false; } + assert autoRefreshTimeoutFunction != null; + if (autoRefreshTimeoutFunction == DEFAULT_AUTO_REFRESH_TIMEOUT_FUNCTION) { return true; } diff --git a/core/src/main/java/com/linecorp/armeria/client/SessionProtocolNegotiationException.java b/core/src/main/java/com/linecorp/armeria/client/SessionProtocolNegotiationException.java index 8314222a6e7..6eced0cba29 100644 --- a/core/src/main/java/com/linecorp/armeria/client/SessionProtocolNegotiationException.java +++ b/core/src/main/java/com/linecorp/armeria/client/SessionProtocolNegotiationException.java @@ -37,7 +37,7 @@ public final class SessionProtocolNegotiationException extends RuntimeException * Creates a new instance with the specified expected {@link SessionProtocol}. */ public SessionProtocolNegotiationException(SessionProtocol expected, @Nullable String reason) { - super("expected: " + requireNonNull(expected, "expected") + ", reason: " + reason); + super(appendReason("expected: " + requireNonNull(expected, "expected"), reason)); this.expected = expected; actual = null; } @@ -48,8 +48,8 @@ public SessionProtocolNegotiationException(SessionProtocol expected, @Nullable S public SessionProtocolNegotiationException(SessionProtocol expected, @Nullable SessionProtocol actual, @Nullable String reason) { - super("expected: " + requireNonNull(expected, "expected") + - ", actual: " + requireNonNull(actual, "actual") + ", reason: " + reason); + super(appendReason("expected: " + requireNonNull(expected, "expected") + + ", actual: " + actual, reason)); this.expected = expected; this.actual = actual; } @@ -78,4 +78,11 @@ public Throwable fillInStackTrace() { } return this; } + + private static String appendReason(String message, @Nullable String reason) { + if (reason == null) { + return message; + } + return message + ", reason: " + reason; + } } diff --git a/core/src/main/java/com/linecorp/armeria/client/UnprocessedRequestException.java b/core/src/main/java/com/linecorp/armeria/client/UnprocessedRequestException.java index 19246913d98..970b4feac44 100644 --- a/core/src/main/java/com/linecorp/armeria/client/UnprocessedRequestException.java +++ b/core/src/main/java/com/linecorp/armeria/client/UnprocessedRequestException.java @@ -55,7 +55,9 @@ private UnprocessedRequestException(Throwable cause) { @Nonnull @Override public Throwable getCause() { - return super.getCause(); + final Throwable cause = super.getCause(); + assert cause != null; + return cause; } @Override diff --git a/core/src/main/java/com/linecorp/armeria/client/WebSocketHttp1ClientChannelHandler.java b/core/src/main/java/com/linecorp/armeria/client/WebSocketHttp1ClientChannelHandler.java index f2c30cc37e0..f80cbe0c4c7 100644 --- a/core/src/main/java/com/linecorp/armeria/client/WebSocketHttp1ClientChannelHandler.java +++ b/core/src/main/java/com/linecorp/armeria/client/WebSocketHttp1ClientChannelHandler.java @@ -189,6 +189,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception return; } + assert res != null; res.startResponse(); final ResponseHeaders responseHeaders = ArmeriaHttpUtil.toArmeria(nettyRes); if (responseHeaders.status() == HttpStatus.SWITCHING_PROTOCOLS) { @@ -215,6 +216,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception final ByteBuf data = (ByteBuf) msg; final int dataLength = data.readableBytes(); if (dataLength > 0) { + assert res != null; final long maxContentLength = res.maxContentLength(); final long writtenBytes = res.writtenBytes(); if (maxContentLength > 0 && writtenBytes > maxContentLength - dataLength) { diff --git a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerMetrics.java b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerMetrics.java index e7e7efce775..e1a3aa20048 100644 --- a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerMetrics.java +++ b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerMetrics.java @@ -51,10 +51,16 @@ final class CircuitBreakerMetrics { parent.gauge(idPrefix.name("state"), idPrefix.tags(), state, AtomicDouble::get); final String requests = idPrefix.name("requests"); - parent.gauge(requests, idPrefix.tags("result", "success"), - latestEventCount, lec -> lec.get().success()); - parent.gauge(requests, idPrefix.tags("result", "failure"), - latestEventCount, lec -> lec.get().failure()); + parent.gauge(requests, idPrefix.tags("result", "success"), latestEventCount, lec -> { + final EventCount eventCount = lec.get(); + assert eventCount != null; + return eventCount.success(); + }); + parent.gauge(requests, idPrefix.tags("result", "failure"), latestEventCount, lec -> { + final EventCount eventCount = lec.get(); + assert eventCount != null; + return eventCount.failure(); + }); final String transitions = idPrefix.name("transitions"); transitionsToClosed = parent.counter(transitions, idPrefix.tags("state", CLOSED.name())); diff --git a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerRuleWithContentBuilder.java b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerRuleWithContentBuilder.java index 69729cb104a..eb9fbdc46e1 100644 --- a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerRuleWithContentBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerRuleWithContentBuilder.java @@ -83,6 +83,7 @@ private CircuitBreakerRuleWithContent build(CircuitBreakerDecision decision) if (content == null) { return NEXT_DECISION; } + assert responseFilter != null; return responseFilter.apply(ctx, content) .handle((matched, cause0) -> { if (cause0 != null) { diff --git a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/DefaultCircuitBreakerClientHandler.java b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/DefaultCircuitBreakerClientHandler.java index 8d789c4ebd5..a66f3f5dc1f 100644 --- a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/DefaultCircuitBreakerClientHandler.java +++ b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/DefaultCircuitBreakerClientHandler.java @@ -23,6 +23,7 @@ import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.common.Request; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.circuitbreaker.CircuitBreakerCallback; final class DefaultCircuitBreakerClientHandler implements CircuitBreakerClientHandler { @@ -35,6 +36,7 @@ final class DefaultCircuitBreakerClientHandler implements CircuitBreakerClientHa this.mapping = mapping; } + @Nullable @Override public CircuitBreakerCallback tryRequest(ClientRequestContext ctx, Request req) { final CircuitBreaker circuitBreaker; diff --git a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/KeyedCircuitBreakerMapping.java b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/KeyedCircuitBreakerMapping.java index 63bb0bfc901..90021d5b165 100644 --- a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/KeyedCircuitBreakerMapping.java +++ b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/KeyedCircuitBreakerMapping.java @@ -39,8 +39,11 @@ */ final class KeyedCircuitBreakerMapping implements CircuitBreakerMapping { - static final CircuitBreakerMapping hostMapping = new KeyedCircuitBreakerMapping( - true, false, false, (host, method, path) -> CircuitBreaker.of(host)); + static final CircuitBreakerMapping hostMapping = + new KeyedCircuitBreakerMapping(true, false, false, (host, method, path) -> { + assert host != null; + return CircuitBreaker.of(host); + }); private final ConcurrentMap mapping = new ConcurrentHashMap<>(); diff --git a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/NonBlockingCircuitBreaker.java b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/NonBlockingCircuitBreaker.java index 3457578f98b..f0309d8dd80 100644 --- a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/NonBlockingCircuitBreaker.java +++ b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/NonBlockingCircuitBreaker.java @@ -71,7 +71,7 @@ public String name() { @Override public void onSuccess() { - final State currentState = state.get(); + final State currentState = state(); if (currentState.isClosed()) { // fires success event final EventCount updatedCount = currentState.counter().onSuccess(); @@ -95,7 +95,7 @@ public void onSuccess(RequestContext ctx) { @Override public void onFailure() { - final State currentState = state.get(); + final State currentState = state(); if (currentState.isClosed()) { // fires failure event final EventCount updatedCount = currentState.counter().onFailure(); @@ -138,7 +138,7 @@ public boolean canRequest() { @Override public boolean tryRequest() { - final State currentState = state.get(); + final State currentState = state(); if (currentState.isClosed()) { // all requests are allowed during CLOSED return true; @@ -166,7 +166,7 @@ public boolean tryRequest() { @Override public CircuitState circuitState() { - return state.get().circuitState; + return state().circuitState; } @Override @@ -264,7 +264,9 @@ private void notifyRequestRejected() { @VisibleForTesting State state() { - return state.get(); + final State state = this.state.get(); + assert state != null; + return state; } @VisibleForTesting @@ -354,11 +356,13 @@ public EventCount count() { return EventCount.ZERO; } + @Nullable @Override public EventCount onSuccess() { return null; } + @Nullable @Override public EventCount onFailure() { return null; diff --git a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/SlidingWindowCounter.java b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/SlidingWindowCounter.java index cd4fa2392b2..b8eb59f3568 100644 --- a/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/SlidingWindowCounter.java +++ b/core/src/main/java/com/linecorp/armeria/client/circuitbreaker/SlidingWindowCounter.java @@ -63,14 +63,18 @@ final class SlidingWindowCounter implements EventCounter { @Override public EventCount count() { - return snapshot.get(); + final EventCount snapshot = this.snapshot.get(); + assert snapshot != null; + return snapshot; } + @Nullable @Override public EventCount onSuccess() { return onEvent(Event.SUCCESS); } + @Nullable @Override public EventCount onFailure() { return onEvent(Event.FAILURE); @@ -81,6 +85,7 @@ private EventCount onEvent(Event event) { final long tickerNanos = ticker.read(); final Bucket currentBucket = current.get(); + assert currentBucket != null; if (tickerNanos < currentBucket.timestamp()) { // if current timestamp is older than bucket's timestamp (maybe race or GC pause?), diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointGroup.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointGroup.java index 4b5b4d505c3..b714cdb9e2f 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointGroup.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointGroup.java @@ -19,10 +19,12 @@ import java.util.List; import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.AbstractListenable; abstract class AbstractEndpointGroup extends AbstractListenable> implements EndpointGroup { + @Nullable @Override protected List latestValue() { if (whenReady().isDone()) { diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelector.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelector.java index 3761e42dc32..613ed369e1b 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelector.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelector.java @@ -227,7 +227,7 @@ public boolean cancel(boolean mayInterruptIfRunning) { } @Override - public boolean complete(Endpoint value) { + public boolean complete(@Nullable Endpoint value) { cleanup(true); return super.complete(value); } diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/CompositeEndpointGroup.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/CompositeEndpointGroup.java index 262db973fde..f495dcf96d7 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/CompositeEndpointGroup.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/CompositeEndpointGroup.java @@ -27,6 +27,7 @@ import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.AsyncCloseable; import com.linecorp.armeria.common.util.AsyncCloseableSupport; import com.linecorp.armeria.common.util.ListenableAsyncCloseable; @@ -104,6 +105,7 @@ public EndpointSelectionStrategy selectionStrategy() { return selectionStrategy; } + @Nullable @Override public Endpoint selectNow(ClientRequestContext ctx) { return selector.selectNow(ctx); diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/DynamicEndpointGroup.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/DynamicEndpointGroup.java index a4a68825303..e28d7fbdced 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/DynamicEndpointGroup.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/DynamicEndpointGroup.java @@ -39,6 +39,7 @@ import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.Endpoint; import com.linecorp.armeria.common.Flags; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; import com.linecorp.armeria.common.util.AsyncCloseableSupport; import com.linecorp.armeria.common.util.EventLoopCheckingFuture; @@ -159,6 +160,7 @@ public final EndpointSelectionStrategy selectionStrategy() { return selectionStrategy; } + @Nullable @Override public final Endpoint selectNow(ClientRequestContext ctx) { return maybeCreateSelector().selectNow(ctx); @@ -198,7 +200,9 @@ private EndpointSelector maybeCreateSelector() { return newSelector; } - return this.selector.get(); + final EndpointSelector oldSelector = this.selector.get(); + assert oldSelector != null; + return oldSelector; } /** @@ -290,6 +294,7 @@ private static boolean hasChanges(List oldEndpoints, List ne return false; } + @Nullable @Override protected List latestValue() { final List endpoints = this.endpoints; diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/EndpointGroup.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/EndpointGroup.java index 55fa5d77478..1e5a54dc087 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/EndpointGroup.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/EndpointGroup.java @@ -29,6 +29,7 @@ import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.Endpoint; import com.linecorp.armeria.client.retry.RetryingClient; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; import com.linecorp.armeria.common.util.AsyncCloseable; import com.linecorp.armeria.common.util.Listenable; @@ -143,6 +144,7 @@ static EndpointGroup of(EndpointSelectionStrategy selectionStrategy, * which was specified when constructing this {@link EndpointGroup}, * or {@code null} if this {@link EndpointGroup} is empty. */ + @Nullable @Override Endpoint selectNow(ClientRequestContext ctx); diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/OrElseEndpointGroup.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/OrElseEndpointGroup.java index e27bbe1dcfd..8c221eb252d 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/OrElseEndpointGroup.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/OrElseEndpointGroup.java @@ -26,6 +26,7 @@ import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.AsyncCloseableSupport; import com.linecorp.armeria.common.util.ListenableAsyncCloseable; @@ -68,6 +69,7 @@ public EndpointSelectionStrategy selectionStrategy() { return first.selectionStrategy(); } + @Nullable @Override public Endpoint selectNow(ClientRequestContext ctx) { return selector.selectNow(ctx); diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/RoundRobinStrategy.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/RoundRobinStrategy.java index f1478b78267..48813621925 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/RoundRobinStrategy.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/RoundRobinStrategy.java @@ -21,6 +21,7 @@ import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.common.annotation.Nullable; final class RoundRobinStrategy implements EndpointSelectionStrategy { @@ -46,6 +47,7 @@ static class RoundRobinSelector extends AbstractEndpointSelector { initialize(); } + @Nullable @Override public Endpoint selectNow(ClientRequestContext ctx) { final List endpoints = group().endpoints(); diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/StickyEndpointSelectionStrategy.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/StickyEndpointSelectionStrategy.java index 26768dc881e..18415603dd2 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/StickyEndpointSelectionStrategy.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/StickyEndpointSelectionStrategy.java @@ -25,6 +25,7 @@ import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.Endpoint; import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.annotation.Nullable; /** * An {@link EndpointSelector} strategy which implements sticky load-balancing using @@ -80,6 +81,7 @@ private static final class StickyEndpointSelector extends AbstractEndpointSelect initialize(); } + @Nullable @Override public Endpoint selectNow(ClientRequestContext ctx) { diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/WeightRampingUpStrategy.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/WeightRampingUpStrategy.java index 9721ce10a60..f71957fe2ec 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/WeightRampingUpStrategy.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/WeightRampingUpStrategy.java @@ -47,6 +47,7 @@ import com.linecorp.armeria.client.Endpoint; import com.linecorp.armeria.client.endpoint.WeightRampingUpStrategy.EndpointsRampingUpEntry.EndpointAndStep; import com.linecorp.armeria.common.CommonPools; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.ListenableAsyncCloseable; import com.linecorp.armeria.common.util.Ticker; import com.linecorp.armeria.internal.common.util.ReentrantShortLock; @@ -166,6 +167,7 @@ private long computeCreateTimestamp(Endpoint endpoint) { return ticker.read(); } + @Nullable @Override public Endpoint selectNow(ClientRequestContext ctx) { return endpointSelector.selectEndpoint(); diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/WeightedRoundRobinStrategy.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/WeightedRoundRobinStrategy.java index 2a92ba93258..d4def3f082a 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/WeightedRoundRobinStrategy.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/WeightedRoundRobinStrategy.java @@ -69,6 +69,7 @@ protected void updateNewEndpoints(List endpoints) { } } + @Nullable @Override public Endpoint selectNow(ClientRequestContext ctx) { final EndpointsAndWeights endpointsAndWeights = this.endpointsAndWeights; diff --git a/core/src/main/java/com/linecorp/armeria/client/endpoint/healthcheck/HttpHealthChecker.java b/core/src/main/java/com/linecorp/armeria/client/endpoint/healthcheck/HttpHealthChecker.java index f46f24608b2..8f5d312b16a 100644 --- a/core/src/main/java/com/linecorp/armeria/client/endpoint/healthcheck/HttpHealthChecker.java +++ b/core/src/main/java/com/linecorp/armeria/client/endpoint/healthcheck/HttpHealthChecker.java @@ -166,7 +166,7 @@ private class HealthCheckResponseSubscriber implements Subscriber { private final ClientRequestContext reqCtx; private final HttpResponse res; - @SuppressWarnings("NotNullFieldNotInitialized") + @Nullable private Subscription subscription; @Nullable private ResponseHeaders responseHeaders; @@ -192,6 +192,8 @@ public void onSubscribe(Subscription subscription) { @Override public void onNext(HttpObject obj) { + assert subscription != null; + if (closeable.isClosing()) { subscription.cancel(); return; diff --git a/core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java b/core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java index bb0b9234382..3a08c219e24 100644 --- a/core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java +++ b/core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java @@ -18,6 +18,8 @@ import java.net.InetSocketAddress; +import com.linecorp.armeria.common.annotation.Nullable; + /** * Represents a direct connection without a proxy. */ @@ -32,6 +34,7 @@ public ProxyType proxyType() { return ProxyType.DIRECT; } + @Nullable @Override public InetSocketAddress proxyAddress() { return null; diff --git a/core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java b/core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java index ddb58e07a0b..e8e5cc277e3 100644 --- a/core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java +++ b/core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java @@ -124,7 +124,9 @@ protected final RetryRule retryRule() { * logical request. */ final RetryConfig mappedRetryConfig(ClientRequestContext ctx) { - return (RetryConfig) ctx.attr(STATE).config; + @SuppressWarnings("unchecked") + final RetryConfig config = (RetryConfig) state(ctx).config; + return config; } /** @@ -176,7 +178,7 @@ protected static void scheduleNextRetry(ClientRequestContext ctx, @SuppressWarnings("MethodMayBeStatic") // Intentionally left non-static for better user experience. protected final boolean setResponseTimeout(ClientRequestContext ctx) { requireNonNull(ctx, "ctx"); - final long responseTimeoutMillis = ctx.attr(STATE).responseTimeoutMillis(); + final long responseTimeoutMillis = state(ctx).responseTimeoutMillis(); if (responseTimeoutMillis < 0) { return false; } else if (responseTimeoutMillis == 0) { @@ -215,7 +217,7 @@ protected final long getNextDelay(ClientRequestContext ctx, Backoff backoff) { protected final long getNextDelay(ClientRequestContext ctx, Backoff backoff, long millisAfterFromServer) { requireNonNull(ctx, "ctx"); requireNonNull(backoff, "backoff"); - final State state = ctx.attr(STATE); + final State state = state(ctx); final int currentAttemptNo = state.currentAttemptNoWith(backoff); if (currentAttemptNo < 0) { @@ -261,6 +263,12 @@ protected static ClientRequestContext newDerivedContext(ClientRequestContext ctx return ClientUtil.newDerivedContext(ctx, req, rpcReq, initialAttempt); } + private static State state(ClientRequestContext ctx) { + final State state = ctx.attr(STATE); + assert state != null; + return state; + } + private static final class State { private final RetryConfig config; diff --git a/core/src/main/java/com/linecorp/armeria/client/retry/Backoff.java b/core/src/main/java/com/linecorp/armeria/client/retry/Backoff.java index 17e612d7638..8b3bab60e2e 100644 --- a/core/src/main/java/com/linecorp/armeria/client/retry/Backoff.java +++ b/core/src/main/java/com/linecorp/armeria/client/retry/Backoff.java @@ -24,6 +24,7 @@ import java.util.function.Supplier; import com.linecorp.armeria.common.Flags; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.Unwrappable; /** @@ -152,6 +153,7 @@ static Backoff of(String specification) { * * @see Unwrappable */ + @Nullable @Override default T as(Class type) { return Unwrappable.super.as(type); diff --git a/core/src/main/java/com/linecorp/armeria/client/retry/RetryConfig.java b/core/src/main/java/com/linecorp/armeria/client/retry/RetryConfig.java index b43a7fc9630..31f7892815d 100644 --- a/core/src/main/java/com/linecorp/armeria/client/retry/RetryConfig.java +++ b/core/src/main/java/com/linecorp/armeria/client/retry/RetryConfig.java @@ -137,9 +137,14 @@ private static void checkArguments(int maxTotalAttempts, long responseTimeoutMil * Converts this {@link RetryConfig} to a mutable {@link RetryConfigBuilder}. */ public RetryConfigBuilder toBuilder() { - final RetryConfigBuilder builder = - retryRuleWithContent != null ? - builder0(retryRuleWithContent).maxContentLength(maxContentLength) : builder0(retryRule); + final RetryConfigBuilder builder; + if (retryRuleWithContent != null) { + builder = builder0(retryRuleWithContent).maxContentLength( + maxContentLength); + } else { + assert retryRule != null; + builder = builder0(retryRule); + } return builder .maxTotalAttempts(maxTotalAttempts) .responseTimeoutMillisForEachAttempt(responseTimeoutMillisForEachAttempt); @@ -197,8 +202,15 @@ public boolean needsContentInRule() { * response trailers. */ public boolean requiresResponseTrailers() { - return needsContentInRule() ? - retryRuleWithContent().requiresResponseTrailers() : retryRule().requiresResponseTrailers(); + if (needsContentInRule()) { + final RetryRuleWithContent rule = retryRuleWithContent(); + assert rule != null; + return rule.requiresResponseTrailers(); + } else { + final RetryRule rule = retryRule(); + assert rule != null; + return rule.requiresResponseTrailers(); + } } /** diff --git a/core/src/main/java/com/linecorp/armeria/client/retry/RetryRuleWithContentBuilder.java b/core/src/main/java/com/linecorp/armeria/client/retry/RetryRuleWithContentBuilder.java index 746103b191b..ca6990bb552 100644 --- a/core/src/main/java/com/linecorp/armeria/client/retry/RetryRuleWithContentBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/client/retry/RetryRuleWithContentBuilder.java @@ -89,6 +89,7 @@ RetryRuleWithContent build(RetryDecision decision) { if (content == null) { return NEXT_DECISION; } + assert responseFilter != null; return responseFilter.apply(ctx, content) .handle((matched, cause0) -> { if (cause0 != null) { diff --git a/core/src/main/java/com/linecorp/armeria/client/retry/RetryingClient.java b/core/src/main/java/com/linecorp/armeria/client/retry/RetryingClient.java index 1c78b39620d..ec0454934e0 100644 --- a/core/src/main/java/com/linecorp/armeria/client/retry/RetryingClient.java +++ b/core/src/main/java/com/linecorp/armeria/client/retry/RetryingClient.java @@ -426,8 +426,13 @@ private void handleResponse(RetryConfig retryConfig, ClientRequest } return; } - final HttpResponse response0 = aggregatedRes != null ? aggregatedRes.toHttpResponse() - : response; + final HttpResponse response0; + if (aggregatedRes != null) { + response0 = aggregatedRes.toHttpResponse(); + } else { + response0 = response; + assert response0 != null; + } handleResponseWithoutContent(retryConfig, ctx, rootReqDuplicator, originalReq, returnedRes, future, derivedCtx, response0, responseCause); }); @@ -517,7 +522,10 @@ private static long getRetryAfterMillis(ClientRequestContext ctx) { private static RetryRule retryRule(RetryConfig retryConfig) { if (retryConfig.needsContentInRule()) { return retryConfig.fromRetryRuleWithContent(); + } else { + final RetryRule rule = retryConfig.retryRule(); + assert rule != null; + return rule; } - return retryConfig.retryRule(); } } diff --git a/core/src/main/java/com/linecorp/armeria/client/retry/RetryingRpcClient.java b/core/src/main/java/com/linecorp/armeria/client/retry/RetryingRpcClient.java index 46caf5a18cb..9968568a458 100644 --- a/core/src/main/java/com/linecorp/armeria/client/retry/RetryingRpcClient.java +++ b/core/src/main/java/com/linecorp/armeria/client/retry/RetryingRpcClient.java @@ -192,6 +192,7 @@ private void doExecute0(ClientRequestContext ctx, RpcRequest req, retryConfig.retryRuleWithContent() : retryConfig.fromRetryRule(); res.handle((unused1, cause) -> { try { + assert retryRule != null; retryRule.shouldRetry(derivedCtx, res, cause).handle((decision, unused3) -> { final Backoff backoff = decision != null ? decision.backoff() : null; if (backoff != null) { diff --git a/core/src/main/java/com/linecorp/armeria/common/ClientCacheControl.java b/core/src/main/java/com/linecorp/armeria/common/ClientCacheControl.java index 2304e972a85..de1a0930b06 100644 --- a/core/src/main/java/com/linecorp/armeria/common/ClientCacheControl.java +++ b/core/src/main/java/com/linecorp/armeria/common/ClientCacheControl.java @@ -254,6 +254,7 @@ public boolean equals(@Nullable Object o) { return false; } + assert o != null; final ClientCacheControl that = (ClientCacheControl) o; return onlyIfCached == that.onlyIfCached && maxStaleSeconds == that.maxStaleSeconds && diff --git a/core/src/main/java/com/linecorp/armeria/common/DefaultAggregatedHttpRequest.java b/core/src/main/java/com/linecorp/armeria/common/DefaultAggregatedHttpRequest.java index 5aa4edc7459..2504369a3a1 100644 --- a/core/src/main/java/com/linecorp/armeria/common/DefaultAggregatedHttpRequest.java +++ b/core/src/main/java/com/linecorp/armeria/common/DefaultAggregatedHttpRequest.java @@ -47,11 +47,13 @@ public String path() { return headers.path(); } + @Nullable @Override public String scheme() { return headers.scheme(); } + @Nullable @Override public String authority() { return headers.authority(); diff --git a/core/src/main/java/com/linecorp/armeria/common/DefaultAggregationOptions.java b/core/src/main/java/com/linecorp/armeria/common/DefaultAggregationOptions.java index 96c5650dd3b..935f9940af9 100644 --- a/core/src/main/java/com/linecorp/armeria/common/DefaultAggregationOptions.java +++ b/core/src/main/java/com/linecorp/armeria/common/DefaultAggregationOptions.java @@ -42,6 +42,7 @@ final class DefaultAggregationOptions implements AggregationOptions { this.cacheResult = cacheResult; } + @Nullable @Override public EventExecutor executor() { return executor; @@ -57,6 +58,7 @@ public boolean preferCached() { return preferCached; } + @Nullable @Override public ByteBufAllocator alloc() { return alloc; diff --git a/core/src/main/java/com/linecorp/armeria/common/DefaultCookie.java b/core/src/main/java/com/linecorp/armeria/common/DefaultCookie.java index 60fe39b052e..6ad287010e0 100644 --- a/core/src/main/java/com/linecorp/armeria/common/DefaultCookie.java +++ b/core/src/main/java/com/linecorp/armeria/common/DefaultCookie.java @@ -89,11 +89,13 @@ public boolean isValueQuoted() { return valueQuoted; } + @Nullable @Override public String domain() { return domain; } + @Nullable @Override public String path() { return path; @@ -114,6 +116,7 @@ public boolean isHttpOnly() { return httpOnly; } + @Nullable @Override public String sameSite() { return sameSite; diff --git a/core/src/main/java/com/linecorp/armeria/common/DefaultDependencyInjector.java b/core/src/main/java/com/linecorp/armeria/common/DefaultDependencyInjector.java index b4a4bc36b0f..dc76512199e 100644 --- a/core/src/main/java/com/linecorp/armeria/common/DefaultDependencyInjector.java +++ b/core/src/main/java/com/linecorp/armeria/common/DefaultDependencyInjector.java @@ -26,6 +26,7 @@ import com.google.common.base.MoreObjects; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.internal.common.util.ReentrantShortLock; final class DefaultDependencyInjector implements DependencyInjector { @@ -45,6 +46,7 @@ final class DefaultDependencyInjector implements DependencyInjector { } } + @Nullable @Override public T getInstance(Class type) { lock.lock(); diff --git a/core/src/main/java/com/linecorp/armeria/common/DefaultRequestHeaders.java b/core/src/main/java/com/linecorp/armeria/common/DefaultRequestHeaders.java index 6cfa8263fd2..7df712e3221 100644 --- a/core/src/main/java/com/linecorp/armeria/common/DefaultRequestHeaders.java +++ b/core/src/main/java/com/linecorp/armeria/common/DefaultRequestHeaders.java @@ -52,6 +52,7 @@ public Locale selectLocale(Iterable supportedLocales) { return super.selectLocale(supportedLocales); } + @Nullable @Override public List acceptLanguages() { return super.acceptLanguages(); diff --git a/core/src/main/java/com/linecorp/armeria/common/Flags.java b/core/src/main/java/com/linecorp/armeria/common/Flags.java index e0f327c82f7..fff4fd96d7b 100644 --- a/core/src/main/java/com/linecorp/armeria/common/Flags.java +++ b/core/src/main/java/com/linecorp/armeria/common/Flags.java @@ -588,6 +588,7 @@ public static TlsEngineType tlsEngineType() { return tlsEngineType; } detectTlsEngineAndDumpOpenSslInfo(); + assert tlsEngineType != null; return tlsEngineType; } @@ -661,6 +662,7 @@ public static boolean dumpOpenSslInfo() { return dumpOpenSslInfo; } detectTlsEngineAndDumpOpenSslInfo(); + assert dumpOpenSslInfo != null; return dumpOpenSslInfo; } diff --git a/core/src/main/java/com/linecorp/armeria/common/HttpHeaderNames.java b/core/src/main/java/com/linecorp/armeria/common/HttpHeaderNames.java index 42892439e15..3f27b8b8fda 100644 --- a/core/src/main/java/com/linecorp/armeria/common/HttpHeaderNames.java +++ b/core/src/main/java/com/linecorp/armeria/common/HttpHeaderNames.java @@ -958,6 +958,7 @@ public final class HttpHeaderNames { } } map = builder.build(); + assert inverseMapBuilder != null; inverseMap = inverseMapBuilder.build(); // inverseMapBuilder is used only when building inverseMap. inverseMapBuilder = null; @@ -965,6 +966,7 @@ public final class HttpHeaderNames { private static AsciiString create(String name) { final AsciiString cached = AsciiString.cached(Ascii.toLowerCase(name)); + assert inverseMapBuilder != null; inverseMapBuilder.put(cached, name); return cached; } diff --git a/core/src/main/java/com/linecorp/armeria/common/HttpResponse.java b/core/src/main/java/com/linecorp/armeria/common/HttpResponse.java index a92db4b2789..8fa33cde424 100644 --- a/core/src/main/java/com/linecorp/armeria/common/HttpResponse.java +++ b/core/src/main/java/com/linecorp/armeria/common/HttpResponse.java @@ -62,6 +62,7 @@ import com.linecorp.armeria.unsafe.PooledObjects; import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.EventLoop; import io.netty.util.concurrent.EventExecutor; /** @@ -193,9 +194,10 @@ static HttpResponse delayed(HttpResponse response, Duration delay, ScheduledExec static HttpResponse delayed(Supplier responseSupplier, Duration delay) { requireNonNull(responseSupplier, "responseSupplier"); requireNonNull(delay, "delay"); - return delayed(responseSupplier, delay, - RequestContext.mapCurrent(RequestContext::eventLoop, - CommonPools.workerGroup()::next)); + final EventLoop executor = RequestContext.mapCurrent(RequestContext::eventLoop, + CommonPools.workerGroup()::next); + assert executor != null; + return delayed(responseSupplier, delay, executor); } /** diff --git a/core/src/main/java/com/linecorp/armeria/common/OrElseDependencyInjector.java b/core/src/main/java/com/linecorp/armeria/common/OrElseDependencyInjector.java index 45836af375e..058a6056b6e 100644 --- a/core/src/main/java/com/linecorp/armeria/common/OrElseDependencyInjector.java +++ b/core/src/main/java/com/linecorp/armeria/common/OrElseDependencyInjector.java @@ -18,6 +18,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.linecorp.armeria.common.annotation.Nullable; + final class OrElseDependencyInjector implements DependencyInjector { private static final Logger logger = LoggerFactory.getLogger(OrElseDependencyInjector.class); @@ -30,6 +32,7 @@ final class OrElseDependencyInjector implements DependencyInjector { this.second = second; } + @Nullable @Override public T getInstance(Class type) { final T instance = first.getInstance(type); diff --git a/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareBlockingTaskExecutor.java b/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareBlockingTaskExecutor.java index a9c2c1254da..f94bc42968d 100644 --- a/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareBlockingTaskExecutor.java +++ b/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareBlockingTaskExecutor.java @@ -21,6 +21,7 @@ import com.google.common.base.MoreObjects; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.BlockingTaskExecutor; final class PropagatingContextAwareBlockingTaskExecutor @@ -39,6 +40,7 @@ private PropagatingContextAwareBlockingTaskExecutor(BlockingTaskExecutor executo super(executor); } + @Nullable @Override RequestContext contextOrNull() { return RequestContext.mapCurrent(Function.identity(), LogRequestContextWarningOnce.INSTANCE); diff --git a/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareExecutor.java b/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareExecutor.java index ebca2db5359..e0a04fe6e40 100644 --- a/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareExecutor.java +++ b/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareExecutor.java @@ -22,6 +22,8 @@ import com.google.common.base.MoreObjects; +import com.linecorp.armeria.common.annotation.Nullable; + final class PropagatingContextAwareExecutor extends AbstractContextAwareExecutor { static PropagatingContextAwareExecutor of(Executor executor) { @@ -37,6 +39,7 @@ private PropagatingContextAwareExecutor(Executor executor) { super(executor); } + @Nullable @Override RequestContext contextOrNull() { return RequestContext.mapCurrent(Function.identity(), LogRequestContextWarningOnce.INSTANCE); diff --git a/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareExecutorService.java b/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareExecutorService.java index ba11e84644f..e88b7ccf895 100644 --- a/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareExecutorService.java +++ b/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareExecutorService.java @@ -22,6 +22,8 @@ import com.google.common.base.MoreObjects; +import com.linecorp.armeria.common.annotation.Nullable; + final class PropagatingContextAwareExecutorService extends AbstractContextAwareExecutorService { @@ -38,6 +40,7 @@ private PropagatingContextAwareExecutorService(ExecutorService executor) { super(executor); } + @Nullable @Override RequestContext contextOrNull() { return RequestContext.mapCurrent(Function.identity(), LogRequestContextWarningOnce.INSTANCE); diff --git a/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareScheduledExecutorService.java b/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareScheduledExecutorService.java index 3bbb0d9be2e..752db561a57 100644 --- a/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareScheduledExecutorService.java +++ b/core/src/main/java/com/linecorp/armeria/common/PropagatingContextAwareScheduledExecutorService.java @@ -22,6 +22,8 @@ import com.google.common.base.MoreObjects; +import com.linecorp.armeria.common.annotation.Nullable; + final class PropagatingContextAwareScheduledExecutorService extends AbstractContextAwareScheduledExecutorService { @@ -38,6 +40,7 @@ private PropagatingContextAwareScheduledExecutorService(ScheduledExecutorService super(executor); } + @Nullable @Override RequestContext contextOrNull() { return RequestContext.mapCurrent(Function.identity(), LogRequestContextWarningOnce.INSTANCE); diff --git a/core/src/main/java/com/linecorp/armeria/common/RequestContextWrapper.java b/core/src/main/java/com/linecorp/armeria/common/RequestContextWrapper.java index d21fc39b2d0..2973d6965b3 100644 --- a/core/src/main/java/com/linecorp/armeria/common/RequestContextWrapper.java +++ b/core/src/main/java/com/linecorp/armeria/common/RequestContextWrapper.java @@ -98,11 +98,13 @@ public Iterator, Object>> ownAttrs() { return unwrap().ownAttrs(); } + @Nullable @Override public V setAttr(AttributeKey key, @Nullable V value) { return unwrap().setAttr(key, value); } + @Nullable @Override public HttpRequest request() { return unwrap().request(); @@ -167,6 +169,7 @@ public String decodedPath() { return unwrap().decodedPath(); } + @Nullable @Override public String query() { return unwrap().query(); diff --git a/core/src/main/java/com/linecorp/armeria/common/Scheme.java b/core/src/main/java/com/linecorp/armeria/common/Scheme.java index 2767fe09283..b0a694ebf24 100644 --- a/core/src/main/java/com/linecorp/armeria/common/Scheme.java +++ b/core/src/main/java/com/linecorp/armeria/common/Scheme.java @@ -114,8 +114,11 @@ public static Scheme parse(String scheme) { * {@link SerializationFormat} and {@link SessionProtocol}. */ public static Scheme of(SerializationFormat serializationFormat, SessionProtocol sessionProtocol) { - return SCHEMES.get(requireNonNull(serializationFormat, "serializationFormat").uriText() + '+' + - requireNonNull(sessionProtocol, "sessionProtocol").uriText()); + final Scheme scheme = SCHEMES.get( + requireNonNull(serializationFormat, "serializationFormat").uriText() + '+' + + requireNonNull(sessionProtocol, "sessionProtocol").uriText()); + assert scheme != null; + return scheme; } private final SerializationFormat serializationFormat; diff --git a/core/src/main/java/com/linecorp/armeria/common/ServerCacheControl.java b/core/src/main/java/com/linecorp/armeria/common/ServerCacheControl.java index 633cc5b6c83..191ede659ec 100644 --- a/core/src/main/java/com/linecorp/armeria/common/ServerCacheControl.java +++ b/core/src/main/java/com/linecorp/armeria/common/ServerCacheControl.java @@ -245,6 +245,7 @@ public boolean equals(@Nullable Object o) { return false; } + assert o != null; final ServerCacheControl that = (ServerCacheControl) o; return cachePublic == that.cachePublic && cachePrivate == that.cachePrivate && diff --git a/core/src/main/java/com/linecorp/armeria/common/StringMultimap.java b/core/src/main/java/com/linecorp/armeria/common/StringMultimap.java index e33e96660dd..0d12d8def59 100644 --- a/core/src/main/java/com/linecorp/armeria/common/StringMultimap.java +++ b/core/src/main/java/com/linecorp/armeria/common/StringMultimap.java @@ -29,7 +29,7 @@ */ package com.linecorp.armeria.common; -import static com.linecorp.armeria.internal.common.util.StringUtil.toBoolean; +import static com.linecorp.armeria.internal.common.util.StringUtil.toBooleanOrNull; import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo; import static java.lang.Math.max; import static java.lang.Math.min; @@ -264,7 +264,7 @@ public Boolean getBoolean(IN_NAME name) { if (v == null) { return null; } - return toBoolean(v, false); + return toBooleanOrNull(v); } @Override @@ -280,7 +280,7 @@ public Boolean getLastBoolean(IN_NAME name) { if (v == null) { return null; } - return toBoolean(v, false); + return toBooleanOrNull(v); } @Override @@ -471,7 +471,7 @@ public final boolean containsObject(IN_NAME name, Object value) { @Override public final boolean containsBoolean(IN_NAME name, boolean value) { return contains(name, actual -> { - final Boolean maybeBoolean = toBoolean(actual, false); + final Boolean maybeBoolean = toBooleanOrNull(actual); return maybeBoolean != null && maybeBoolean == value; }); } diff --git a/core/src/main/java/com/linecorp/armeria/common/SystemPropertyFlagsProvider.java b/core/src/main/java/com/linecorp/armeria/common/SystemPropertyFlagsProvider.java index 572b6711a1d..2ea4026abdc 100644 --- a/core/src/main/java/com/linecorp/armeria/common/SystemPropertyFlagsProvider.java +++ b/core/src/main/java/com/linecorp/armeria/common/SystemPropertyFlagsProvider.java @@ -67,6 +67,7 @@ public String name() { return "sysprops"; } + @Nullable @Override public Sampler> verboseExceptionSampler() { final String spec = getNormalized("verboseExceptions"); @@ -82,16 +83,19 @@ public Sampler> verboseExceptionSampler() { return new ExceptionSampler(spec); } + @Nullable @Override public Boolean verboseSocketExceptions() { return getBoolean("verboseSocketExceptions"); } + @Nullable @Override public Boolean verboseResponses() { return getBoolean("verboseResponses"); } + @Nullable @Override public RequestContextStorageProvider requestContextStorageProvider() { final String providerFqcn = System.getProperty(PREFIX + "requestContextStorageProvider"); @@ -122,11 +126,13 @@ public RequestContextStorageProvider requestContextStorageProvider() { return matchedCandidates.get(0); } + @Nullable @Override public Boolean warnNettyVersions() { return getBoolean("warnNettyVersions"); } + @Nullable @Override public TransportType transportType() { final String strTransportType = getNormalized("transportType"); @@ -145,11 +151,13 @@ public TransportType transportType() { } } + @Nullable @Override public Boolean useOpenSsl() { return getBoolean("useOpenSsl"); } + @Nullable @Override public TlsEngineType tlsEngineType() { final String strTlsEngineType = getNormalized("tlsEngineType"); @@ -167,201 +175,241 @@ public TlsEngineType tlsEngineType() { } } + @Nullable @Override public Boolean dumpOpenSslInfo() { return getBoolean("dumpOpenSslInfo"); } + @Nullable @Override public Integer maxNumConnections() { return getInt("maxNumConnections"); } + @Nullable @Override public Integer numCommonWorkers(TransportType transportType) { return getInt("numCommonWorkers"); } + @Nullable @Override public Integer numCommonBlockingTaskThreads() { return getInt("numCommonBlockingTaskThreads"); } + @Nullable @Override public Long defaultMaxRequestLength() { return getLong("defaultMaxRequestLength"); } + @Nullable @Override public Long defaultMaxResponseLength() { return getLong("defaultMaxResponseLength"); } + @Nullable @Override public Long defaultRequestTimeoutMillis() { return getLong("defaultRequestTimeoutMillis"); } + @Nullable @Override public Long defaultResponseTimeoutMillis() { return getLong("defaultResponseTimeoutMillis"); } + @Nullable @Override public Long defaultConnectTimeoutMillis() { return getLong("defaultConnectTimeoutMillis"); } + @Nullable @Override public Long defaultWriteTimeoutMillis() { return getLong("defaultWriteTimeoutMillis"); } + @Nullable @Override public Long defaultServerIdleTimeoutMillis() { return getLong("defaultServerIdleTimeoutMillis"); } + @Nullable @Override public Long defaultClientIdleTimeoutMillis() { return getLong("defaultClientIdleTimeoutMillis"); } + @Nullable @Override public Integer defaultHttp1MaxInitialLineLength() { return getInt("defaultHttp1MaxInitialLineLength"); } + @Nullable @Override public Integer defaultHttp1MaxHeaderSize() { return getInt("defaultHttp1MaxHeaderSize"); } + @Nullable @Override public Integer defaultHttp1MaxChunkSize() { return getInt("defaultHttp1MaxChunkSize"); } + @Nullable @Override public Boolean defaultUseHttp2Preface() { return getBoolean("defaultUseHttp2Preface"); } + @Nullable @Override public Boolean defaultPreferHttp1() { return getBoolean("preferHttp1"); } + @Nullable @Override public Boolean defaultUseHttp2WithoutAlpn() { return getBoolean("defaultUseHttp2WithoutAlpn"); } + @Nullable @Override public Boolean defaultUseHttp1Pipelining() { return getBoolean("defaultUseHttp1Pipelining"); } + @Nullable @Override public Long defaultPingIntervalMillis() { return getLong("defaultPingIntervalMillis"); } + @Nullable @Override public Integer defaultMaxServerNumRequestsPerConnection() { return getInt("defaultMaxServerNumRequestsPerConnection"); } + @Nullable @Override public Integer defaultMaxClientNumRequestsPerConnection() { return getInt("defaultMaxClientNumRequestsPerConnection"); } + @Nullable @Override public Long defaultMaxServerConnectionAgeMillis() { return getLong("defaultMaxServerConnectionAgeMillis"); } + @Nullable @Override public Long defaultMaxClientConnectionAgeMillis() { return getLong("defaultMaxClientConnectionAgeMillis"); } + @Nullable @Override public Long defaultServerConnectionDrainDurationMicros() { return getLong("defaultServerConnectionDrainDurationMicros"); } + @Nullable @Override public Long defaultClientHttp2GracefulShutdownTimeoutMillis() { return getLong("defaultClientHttp2GracefulShutdownTimeoutMillis"); } + @Nullable @Override public Integer defaultHttp2InitialConnectionWindowSize() { return getInt("defaultHttp2InitialConnectionWindowSize"); } + @Nullable @Override public Integer defaultHttp2InitialStreamWindowSize() { return getInt("defaultHttp2InitialStreamWindowSize"); } + @Nullable @Override public Integer defaultHttp2MaxFrameSize() { return getInt("defaultHttp2MaxFrameSize"); } + @Nullable @Override public Long defaultHttp2MaxStreamsPerConnection() { return getLong("defaultHttp2MaxStreamsPerConnection"); } + @Nullable @Override public Long defaultHttp2MaxHeaderListSize() { return getLong("defaultHttp2MaxHeaderListSize"); } + @Nullable @Override public Integer defaultServerHttp2MaxResetFramesPerMinute() { return getInt("defaultServerHttp2MaxResetFramesPerMinute"); } + @Nullable @Override public String defaultBackoffSpec() { return getNormalized("defaultBackoffSpec"); } + @Nullable @Override public Integer defaultMaxTotalAttempts() { return getInt("defaultMaxTotalAttempts"); } + @Nullable @Override - public @Nullable Long defaultRequestAutoAbortDelayMillis() { + public Long defaultRequestAutoAbortDelayMillis() { return getLong("defaultRequestAutoAbortDelayMillis"); } + @Nullable @Override public String routeCacheSpec() { return getNormalized("routeCacheSpec"); } + @Nullable @Override public String routeDecoratorCacheSpec() { return getNormalized("routeDecoratorCacheSpec"); } + @Nullable @Override public String parsedPathCacheSpec() { return getNormalized("parsedPathCacheSpec"); } + @Nullable @Override public String headerValueCacheSpec() { return getNormalized("headerValueCacheSpec"); } + @Nullable @Override public List cachedHeaders() { final String val = getNormalized("cachedHeaders"); @@ -371,16 +419,19 @@ public List cachedHeaders() { return CSV_SPLITTER.splitToList(val); } + @Nullable @Override public String fileServiceCacheSpec() { return getNormalized("fileServiceCacheSpec"); } + @Nullable @Override public String dnsCacheSpec() { return getNormalized("dnsCacheSpec"); } + @Nullable @Override public Predicate preferredIpV4Addresses() { final String val = getNormalized("preferredIpV4Addresses"); @@ -417,36 +468,43 @@ public Predicate preferredIpV4Addresses() { } } + @Nullable @Override public Boolean useJdkDnsResolver() { return getBoolean("useJdkDnsResolver"); } + @Nullable @Override public Boolean reportBlockedEventLoop() { return getBoolean("reportBlockedEventLoop"); } + @Nullable @Override public Boolean reportMaskedRoutes() { return getBoolean("reportMaskedRoutes"); } + @Nullable @Override public Boolean validateHeaders() { return getBoolean("validateHeaders"); } + @Nullable @Override public Boolean tlsAllowUnsafeCiphers() { return getBoolean("tlsAllowUnsafeCiphers"); } + @Nullable @Override public Integer defaultMaxClientHelloLength() { return getInt("defaultMaxClientHelloLength"); } + @Nullable @Override public Set transientServiceOptions() { final String val = getNormalized("transientServiceOptions"); @@ -459,26 +517,31 @@ public Set transientServiceOptions() { .collect(toImmutableSet())); } + @Nullable @Override public Boolean useDefaultSocketOptions() { return getBoolean("useDefaultSocketOptions"); } + @Nullable @Override public Boolean useLegacyRouteDecoratorOrdering() { return getBoolean("useLegacyRouteDecoratorOrdering"); } + @Nullable @Override public Boolean allowDoubleDotsInQueryString() { return getBoolean("allowDoubleDotsInQueryString"); } + @Nullable @Override public Boolean allowSemicolonInPathComponent() { return getBoolean("allowSemicolonInPathComponent"); } + @Nullable @Override public Path defaultMultipartUploadsLocation() { return getAndParse("defaultMultipartUploadsLocation", Paths::get); @@ -502,6 +565,7 @@ public MultipartRemovalStrategy defaultMultipartRemovalStrategy() { } } + @Nullable @Override public Sampler requestContextLeakDetectionSampler() { final String spec = getNormalized("requestContextLeakDetectionSampler"); @@ -516,18 +580,21 @@ public Sampler requestContextLeakDetectionSampler() { } } + @Nullable @Override public Long defaultUnhandledExceptionsReportIntervalMillis() { return getLong("defaultUnhandledExceptionsReportIntervalMillis"); } + @Nullable @Override public Long defaultHttp1ConnectionCloseDelayMillis() { return getLong("defaultHttp1ConnectionCloseDelayMillis"); } + @Nullable @Override - public @Nullable Long defaultUnloggedExceptionsReportIntervalMillis() { + public Long defaultUnloggedExceptionsReportIntervalMillis() { return getLong("defaultUnloggedExceptionsReportIntervalMillis"); } diff --git a/core/src/main/java/com/linecorp/armeria/common/annotation/Nullable.java b/core/src/main/java/com/linecorp/armeria/common/annotation/Nullable.java index c67596973a1..cec410020e9 100644 --- a/core/src/main/java/com/linecorp/armeria/common/annotation/Nullable.java +++ b/core/src/main/java/com/linecorp/armeria/common/annotation/Nullable.java @@ -18,7 +18,6 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -32,7 +31,6 @@ * @see NonNullByDefault */ @Documented -@Inherited @TypeQualifierNickname @Nonnull(when = When.MAYBE) @Retention(RetentionPolicy.RUNTIME) diff --git a/core/src/main/java/com/linecorp/armeria/common/auth/AuthUtil.java b/core/src/main/java/com/linecorp/armeria/common/auth/AuthUtil.java index 76d4735b660..1957e6f86ef 100644 --- a/core/src/main/java/com/linecorp/armeria/common/auth/AuthUtil.java +++ b/core/src/main/java/com/linecorp/armeria/common/auth/AuthUtil.java @@ -24,8 +24,15 @@ final class AuthUtil { * Compares two specified strings in the secure way. */ static boolean secureEquals(@Nullable String a, @Nullable String b) { - final int aLength = a != null ? a.length() : 0; - final int bLength = b != null ? b.length() : 0; + if (a == null) { + return b == null; + } + if (b == null) { + return false; + } + + final int aLength = a.length(); + final int bLength = b.length(); final int length = Math.min(aLength, bLength); int result = 0; for (int i = 0; i < length; i++) { diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java b/core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java index b91bc58a247..7cd223bb086 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java @@ -697,6 +697,7 @@ public long requestStartTimeNanos() { return requestStartTimeNanos; } + @Nullable @Override public Long requestFirstBytesTransferredTimeNanos() { ensureAvailable(RequestLogProperty.REQUEST_FIRST_BYTES_TRANSFERRED_TIME); @@ -715,6 +716,7 @@ public long requestDurationNanos() { return requestEndTimeNanos - requestStartTimeNanos; } + @Nullable @Override public Throwable requestCause() { ensureAvailable(RequestLogProperty.REQUEST_CAUSE); @@ -766,12 +768,14 @@ private void maybeSetScheme() { updateFlags(RequestLogProperty.SCHEME); } + @Nullable @Override public Channel channel() { ensureAvailable(RequestLogProperty.SESSION); return channel; } + @Nullable @Override public SSLSession sslSession() { ensureAvailable(RequestLogProperty.SESSION); @@ -876,6 +880,7 @@ public String fullName() { } } + @Nullable @Override public String authenticatedUser() { ensureAvailable(RequestLogProperty.AUTHENTICATED_USER); @@ -968,6 +973,7 @@ public void requestHeaders(RequestHeaders requestHeaders) { updateFlags(RequestLogProperty.REQUEST_HEADERS); } + @Nullable @Override public Object requestContent() { ensureAvailable(RequestLogProperty.REQUEST_CONTENT); @@ -993,12 +999,14 @@ public void requestContent(@Nullable Object requestContent, @Nullable Object raw } } + @Nullable @Override public Object rawRequestContent() { ensureAvailable(RequestLogProperty.REQUEST_CONTENT); return rawRequestContent; } + @Nullable @Override public String requestContentPreview() { ensureAvailable(RequestLogProperty.REQUEST_CONTENT_PREVIEW); @@ -1136,6 +1144,7 @@ private void setNamesIfAbsent() { // Set serviceName from ServiceType or innermost class name if (newServiceName == null) { if (config != null) { + assert sctx != null; newServiceName = ServiceNaming.fullTypeName().serviceName(sctx); } else if (rpcReq != null) { newServiceName = rpcReq.serviceName(); @@ -1198,6 +1207,7 @@ public long responseStartTimeNanos() { return responseStartTimeNanos; } + @Nullable @Override public Long responseFirstBytesTransferredTimeNanos() { ensureAvailable(RequestLogProperty.RESPONSE_FIRST_BYTES_TRANSFERRED_TIME); @@ -1222,6 +1232,7 @@ public long totalDurationNanos() { return responseEndTimeNanos - requestStartTimeNanos; } + @Nullable @Override public Throwable responseCause() { ensureAvailable(RequestLogProperty.RESPONSE_CAUSE); @@ -1315,6 +1326,7 @@ public void responseHeaders(ResponseHeaders responseHeaders) { updateFlags(RequestLogProperty.RESPONSE_HEADERS); } + @Nullable @Override public Object responseContent() { ensureAvailable(RequestLogProperty.RESPONSE_CONTENT); @@ -1343,6 +1355,7 @@ public void responseContent(@Nullable Object responseContent, @Nullable Object r updateFlags(RequestLogProperty.RESPONSE_CONTENT); } + @Nullable @Override public String responseContentPreview() { ensureAvailable(RequestLogProperty.RESPONSE_CONTENT_PREVIEW); @@ -1359,6 +1372,7 @@ public void responseContentPreview(@Nullable String responseContentPreview) { updateFlags(RequestLogProperty.RESPONSE_CONTENT_PREVIEW); } + @Nullable @Override public Object rawResponseContent() { ensureAvailable(RequestLogProperty.RESPONSE_CONTENT); @@ -1570,7 +1584,6 @@ public boolean isAvailable(Iterable properties) { return true; } - @Nullable @Override public RequestLog getIfAvailable(RequestLogProperty... properties) { requireNonNull(properties, "properties"); @@ -1578,7 +1591,6 @@ public RequestLog getIfAvailable(RequestLogProperty... properties) { return this; } - @Nullable @Override public RequestLog getIfAvailable(Iterable properties) { requireNonNull(properties, "properties"); @@ -1684,6 +1696,7 @@ public long requestStartTimeNanos() { return requestStartTimeNanos; } + @Nullable @Override public Long requestFirstBytesTransferredTimeNanos() { return requestFirstBytesTransferredTimeNanosSet ? requestFirstBytesTransferredTimeNanos : null; @@ -1748,6 +1761,7 @@ public String serviceName() { @Override public String name() { + assert name != null; return name; } @@ -1756,6 +1770,7 @@ public String fullName() { return DefaultRequestLog.this.fullName(); } + @Nullable @Override public String authenticatedUser() { return authenticatedUser; @@ -1810,6 +1825,7 @@ public long responseStartTimeNanos() { return responseStartTimeNanos; } + @Nullable @Override public Long responseFirstBytesTransferredTimeNanos() { return responseFirstBytesTransferredTimeNanosSet ? responseFirstBytesTransferredTimeNanos : null; diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/LoggingDecoratorBuilder.java b/core/src/main/java/com/linecorp/armeria/common/logging/LoggingDecoratorBuilder.java index 5216a09b39d..6eacfed7396 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/LoggingDecoratorBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/LoggingDecoratorBuilder.java @@ -73,8 +73,7 @@ protected LoggingDecoratorBuilder defaultLogger(Logger defaultLogger) { */ @Deprecated public LoggingDecoratorBuilder logger(Logger logger) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.logger(logger); + maybeCreateLogWriterBuilder().logger(logger); return this; } @@ -87,8 +86,7 @@ public LoggingDecoratorBuilder logger(Logger logger) { @Deprecated public LoggingDecoratorBuilder logger(String loggerName) { requireNonNull(loggerName, "loggerName"); - maybeCreateLogWriterBuilder(); - logWriterBuilder.logger(LoggerFactory.getLogger(loggerName)); + maybeCreateLogWriterBuilder().logger(LoggerFactory.getLogger(loggerName)); return this; } @@ -111,8 +109,7 @@ protected final Logger logger() { */ @Deprecated public LoggingDecoratorBuilder requestLogLevel(LogLevel requestLogLevel) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.requestLogLevel(requestLogLevel); + maybeCreateLogWriterBuilder().requestLogLevel(requestLogLevel); return this; } @@ -123,8 +120,7 @@ public LoggingDecoratorBuilder requestLogLevel(LogLevel requestLogLevel) { */ @Deprecated public LoggingDecoratorBuilder requestLogLevel(Class clazz, LogLevel requestLogLevel) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.requestLogLevel(clazz, requestLogLevel); + maybeCreateLogWriterBuilder().requestLogLevel(clazz, requestLogLevel); return this; } @@ -137,8 +133,7 @@ public LoggingDecoratorBuilder requestLogLevel(Class clazz, public LoggingDecoratorBuilder requestLogLevelMapper( Function requestLogLevelMapper) { requireNonNull(requestLogLevelMapper, "requestLogLevelMapper"); - maybeCreateLogWriterBuilder(); - logWriterBuilder.requestLogLevelMapper(requestLogLevelMapper::apply); + maybeCreateLogWriterBuilder().requestLogLevelMapper(requestLogLevelMapper::apply); return this; } @@ -149,8 +144,7 @@ public LoggingDecoratorBuilder requestLogLevelMapper( */ @Deprecated public LoggingDecoratorBuilder requestLogLevelMapper(RequestLogLevelMapper requestLogLevelMapper) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.requestLogLevelMapper(requestLogLevelMapper); + maybeCreateLogWriterBuilder().requestLogLevelMapper(requestLogLevelMapper); return this; } @@ -176,8 +170,7 @@ protected final RequestLogLevelMapper requestLogLevelMapper() { */ @Deprecated public LoggingDecoratorBuilder responseLogLevel(HttpStatus status, LogLevel logLevel) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.responseLogLevel(status, logLevel); + maybeCreateLogWriterBuilder().responseLogLevel(status, logLevel); return this; } @@ -189,8 +182,7 @@ public LoggingDecoratorBuilder responseLogLevel(HttpStatus status, LogLevel logL */ @Deprecated public LoggingDecoratorBuilder responseLogLevel(HttpStatusClass statusClass, LogLevel logLevel) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.responseLogLevel(statusClass, logLevel); + maybeCreateLogWriterBuilder().responseLogLevel(statusClass, logLevel); return this; } @@ -201,8 +193,7 @@ public LoggingDecoratorBuilder responseLogLevel(HttpStatusClass statusClass, Log */ @Deprecated public LoggingDecoratorBuilder responseLogLevel(Class clazz, LogLevel logLevel) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.responseLogLevel(clazz, logLevel); + maybeCreateLogWriterBuilder().responseLogLevel(clazz, logLevel); return this; } @@ -214,8 +205,7 @@ public LoggingDecoratorBuilder responseLogLevel(Class clazz */ @Deprecated public LoggingDecoratorBuilder successfulResponseLogLevel(LogLevel successfulResponseLogLevel) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.successfulResponseLogLevel(successfulResponseLogLevel); + maybeCreateLogWriterBuilder().successfulResponseLogLevel(successfulResponseLogLevel); return this; } @@ -227,8 +217,7 @@ public LoggingDecoratorBuilder successfulResponseLogLevel(LogLevel successfulRes */ @Deprecated public LoggingDecoratorBuilder failureResponseLogLevel(LogLevel failedResponseLogLevel) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.failureResponseLogLevel(failedResponseLogLevel); + maybeCreateLogWriterBuilder().failureResponseLogLevel(failedResponseLogLevel); return this; } @@ -241,7 +230,6 @@ public LoggingDecoratorBuilder failureResponseLogLevel(LogLevel failedResponseLo public LoggingDecoratorBuilder responseLogLevelMapper( Function responseLogLevelMapper) { requireNonNull(responseLogLevelMapper, "responseLogLevelMapper"); - maybeCreateLogWriterBuilder(); return responseLogLevelMapper(responseLogLevelMapper::apply); } @@ -252,8 +240,7 @@ public LoggingDecoratorBuilder responseLogLevelMapper( */ @Deprecated public LoggingDecoratorBuilder responseLogLevelMapper(ResponseLogLevelMapper responseLogLevelMapper) { - maybeCreateLogWriterBuilder(); - logWriterBuilder.responseLogLevelMapper(responseLogLevelMapper); + maybeCreateLogWriterBuilder().responseLogLevelMapper(responseLogLevelMapper); return this; } @@ -284,8 +271,8 @@ public LoggingDecoratorBuilder requestHeadersSanitizer( BiFunction requestHeadersSanitizer) { requireNonNull(requestHeadersSanitizer, "requestHeadersSanitizer"); - maybeCreateLogWriterBuilder(); - logFormatterBuilder.requestHeadersSanitizer(convertToStringSanitizer(requestHeadersSanitizer)); + maybeCreateLogFormatterBuilder().requestHeadersSanitizer( + convertToStringSanitizer(requestHeadersSanitizer)); return this; } @@ -316,9 +303,9 @@ public LoggingDecoratorBuilder requestHeadersSanitizer( public LoggingDecoratorBuilder responseHeadersSanitizer( BiFunction responseHeadersSanitizer) { - maybeCreateLogWriterBuilder(); requireNonNull(responseHeadersSanitizer, "responseHeadersSanitizer"); - logFormatterBuilder.responseHeadersSanitizer(convertToStringSanitizer(responseHeadersSanitizer)); + maybeCreateLogFormatterBuilder().responseHeadersSanitizer( + convertToStringSanitizer(responseHeadersSanitizer)); return this; } @@ -349,8 +336,8 @@ public LoggingDecoratorBuilder requestTrailersSanitizer( BiFunction requestTrailersSanitizer) { requireNonNull(requestTrailersSanitizer, "requestTrailersSanitizer"); - maybeCreateLogWriterBuilder(); - logFormatterBuilder.requestTrailersSanitizer(convertToStringSanitizer(requestTrailersSanitizer)); + maybeCreateLogFormatterBuilder().requestTrailersSanitizer( + convertToStringSanitizer(requestTrailersSanitizer)); return this; } @@ -381,8 +368,8 @@ public LoggingDecoratorBuilder responseTrailersSanitizer( BiFunction responseTrailersSanitizer) { requireNonNull(responseTrailersSanitizer, "responseTrailersSanitizer"); - maybeCreateLogWriterBuilder(); - logFormatterBuilder.responseTrailersSanitizer(convertToStringSanitizer(responseTrailersSanitizer)); + maybeCreateLogFormatterBuilder().responseTrailersSanitizer( + convertToStringSanitizer(responseTrailersSanitizer)); return this; } @@ -444,8 +431,8 @@ public LoggingDecoratorBuilder requestContentSanitizer( BiFunction requestContentSanitizer) { requireNonNull(requestContentSanitizer, "requestContentSanitizer"); - maybeCreateLogWriterBuilder(); - logFormatterBuilder.requestContentSanitizer(convertToStringSanitizer(requestContentSanitizer)); + maybeCreateLogFormatterBuilder().requestContentSanitizer( + convertToStringSanitizer(requestContentSanitizer)); return this; } @@ -477,8 +464,8 @@ public LoggingDecoratorBuilder responseContentSanitizer( BiFunction responseContentSanitizer) { requireNonNull(responseContentSanitizer, "responseContentSanitizer"); - maybeCreateLogWriterBuilder(); - logFormatterBuilder.responseContentSanitizer(convertToStringSanitizer(responseContentSanitizer)); + maybeCreateLogFormatterBuilder().responseContentSanitizer( + convertToStringSanitizer(responseContentSanitizer)); return this; } @@ -538,8 +525,8 @@ public LoggingDecoratorBuilder responseCauseSanitizer( BiFunction responseCauseSanitizer) { requireNonNull(responseCauseSanitizer, "responseCauseSanitizer"); - maybeCreateLogWriterBuilder(); - logWriterBuilder.responseCauseFilter((ctx, cause) -> responseCauseSanitizer.apply(ctx, cause) != null); + maybeCreateLogWriterBuilder().responseCauseFilter( + (ctx, cause) -> responseCauseSanitizer.apply(ctx, cause) != null); return this; } @@ -565,8 +552,7 @@ public LoggingDecoratorBuilder responseCauseSanitizer( @Deprecated public LoggingDecoratorBuilder responseCauseFilter(Predicate responseCauseFilter) { requireNonNull(responseCauseFilter, "responseCauseFilter"); - maybeCreateLogWriterBuilder(); - logWriterBuilder.responseCauseFilter((ctx, cause) -> responseCauseFilter.test(cause)); + maybeCreateLogWriterBuilder().responseCauseFilter((ctx, cause) -> responseCauseFilter.test(cause)); return this; } @@ -612,14 +598,20 @@ protected final LogWriter logWriter() { return logWriterBuilder.build(); } - private void maybeCreateLogWriterBuilder() { + private LogWriterBuilder maybeCreateLogWriterBuilder() { if (logWriter != null) { throw new IllegalStateException("The logWriter and the log properties cannot be set together."); } - if (logWriterBuilder != null) { - return; + if (logWriterBuilder == null) { + logWriterBuilder = LogWriter.builder(); + logFormatterBuilder = LogFormatter.builderForText(); } - logWriterBuilder = LogWriter.builder(); - logFormatterBuilder = LogFormatter.builderForText(); + return logWriterBuilder; + } + + private TextLogFormatterBuilder maybeCreateLogFormatterBuilder() { + maybeCreateLogWriterBuilder(); + assert logFormatterBuilder != null; + return logFormatterBuilder; } } diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/RequestScopedMdc.java b/core/src/main/java/com/linecorp/armeria/common/logging/RequestScopedMdc.java index 275db52eea2..59355a7b3d8 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/RequestScopedMdc.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/RequestScopedMdc.java @@ -138,7 +138,7 @@ public final class RequestScopedMdc { try { oldAdapterGetPropertyMap = MethodHandles.publicLookup() - .findVirtual(oldAdapter.getClass(), "getPropertyMap", + .findVirtual(delegate.getClass(), "getPropertyMap", MethodType.methodType(Map.class)) .bindTo(delegate); @SuppressWarnings("unchecked") diff --git a/core/src/main/java/com/linecorp/armeria/common/multipart/MimeParser.java b/core/src/main/java/com/linecorp/armeria/common/multipart/MimeParser.java index 822cd29a2db..5ccd0ce8f5d 100644 --- a/core/src/main/java/com/linecorp/armeria/common/multipart/MimeParser.java +++ b/core/src/main/java/com/linecorp/armeria/common/multipart/MimeParser.java @@ -213,6 +213,9 @@ void parse() { case HEADERS: logger.trace("state={}", State.HEADERS); + assert bodyPartHeadersBuilder != null; + assert bodyPartBuilder != null; + final String headerLine = readHeaderLine(); if (headerLine == null) { // Need more data; DecodedHttpStreamMessage will handle. @@ -241,6 +244,8 @@ void parse() { case BODY: logger.trace("state={}", State.BODY); + assert bodyPartPublisher != null; + final ByteBuf bodyContent = readBody(); if (bodyContent == NEED_MORE) { final BodyPartPublisher currentPublisher = bodyPartPublisher; @@ -267,6 +272,7 @@ void parse() { } else { state = State.START_PART; } + assert bodyPartPublisher != null; bodyPartPublisher.close(); bodyPartPublisher = null; bodyPartHeadersBuilder = null; diff --git a/core/src/main/java/com/linecorp/armeria/common/multipart/MultipartDecoder.java b/core/src/main/java/com/linecorp/armeria/common/multipart/MultipartDecoder.java index 8a310991109..c0672443a8d 100644 --- a/core/src/main/java/com/linecorp/armeria/common/multipart/MultipartDecoder.java +++ b/core/src/main/java/com/linecorp/armeria/common/multipart/MultipartDecoder.java @@ -144,6 +144,8 @@ public void subscribe(Subscriber subscriber, EventExecutor exe if (!delegatedSubscriberUpdater.compareAndSet(this, null, multipartSubscriber)) { // Avoid calling method on late multipartSubscriber. // Because it's not static, so it will affect MultipartDecoder. + final MultipartSubscriber delegatedSubscriber = this.delegatedSubscriber; + assert delegatedSubscriber != null; SubscriberUtil.failLateSubscriber(executor, subscriber, delegatedSubscriber.subscriber); return; } @@ -279,13 +281,13 @@ public void request(long n) { } } - @SuppressWarnings("UnstableApiUsage") private void request0(long n) { final long oldDemand = demandOfMultipart; demandOfMultipart = LongMath.saturatedAdd(oldDemand, n); if (oldDemand == 0) { // We want first body publisher if (currentExposedBodyPartPublisher == null) { + assert subscription != null; //This will trigger DecodedHttpStreamMessage's upstream. subscription.request(1); } else { diff --git a/core/src/main/java/com/linecorp/armeria/common/multipart/MultipartEncoder.java b/core/src/main/java/com/linecorp/armeria/common/multipart/MultipartEncoder.java index 266eefcf7fd..04c7178b93b 100644 --- a/core/src/main/java/com/linecorp/armeria/common/multipart/MultipartEncoder.java +++ b/core/src/main/java/com/linecorp/armeria/common/multipart/MultipartEncoder.java @@ -90,7 +90,9 @@ public CompletableFuture whenComplete() { if (completionFutureUpdater.compareAndSet(this, null, completionFuture)) { return completionFuture; } else { - return this.completionFuture; + final CompletableFuture oldFuture = this.completionFuture; + assert oldFuture != null; + return oldFuture; } } @@ -243,7 +245,9 @@ public void onSubscribe(Subscription subscription) { } else { if (!completionFutureUpdater .compareAndSet(MultipartEncoder.this, null, newEmitter.whenComplete())) { - completeAsync(newEmitter.whenComplete(), MultipartEncoder.this.completionFuture); + final CompletableFuture oldFuture = MultipartEncoder.this.completionFuture; + assert oldFuture != null; + completeAsync(newEmitter.whenComplete(), oldFuture); } } @@ -268,6 +272,8 @@ private void completeAsync(CompletableFuture first, CompletableFuture> emitter = MultipartEncoder.this.emitter; + assert emitter != null; emitter.write(createBodyPartPublisher(bodyPart)); } @@ -280,6 +286,8 @@ public void onError(Throwable cause) { } closed = true; + final StreamWriter> emitter = MultipartEncoder.this.emitter; + assert emitter != null; emitter.abort(cause); } @@ -290,6 +298,8 @@ public void onComplete() { } closed = true; + final StreamWriter> emitter = MultipartEncoder.this.emitter; + assert emitter != null; emitter.close(); } } diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/AsyncFileWriter.java b/core/src/main/java/com/linecorp/armeria/common/stream/AsyncFileWriter.java index 5fff05e0539..fa9679476a5 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/AsyncFileWriter.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/AsyncFileWriter.java @@ -95,12 +95,14 @@ public void onSubscribe(Subscription subscription) { public void onNext(HttpData httpData) { if (httpData.isEmpty()) { httpData.close(); + assert subscription != null; subscription.request(1); } else { final ByteBuf byteBuf = httpData.byteBuf(); final ByteBuffer byteBuffer = byteBuf.nioBuffer(); writing = true; try { + assert fileChannel != null; fileChannel.write(byteBuffer, position, Maps.immutableEntry(byteBuffer, byteBuf), this); } catch (Throwable ex) { maybeCloseFileChannel(ex, false); @@ -130,12 +132,14 @@ CompletableFuture whenComplete() { public void completed(Integer result, Entry attachment) { assert subscription != null; eventExecutor.execute(() -> { + assert subscription != null; final ByteBuf byteBuf = attachment.getValue(); if (result > -1) { position += result; final ByteBuffer byteBuffer = attachment.getKey(); if (byteBuffer.hasRemaining()) { try { + assert fileChannel != null; fileChannel.write(byteBuffer, position, attachment, this); } catch (Throwable ex) { byteBuf.release(); @@ -178,6 +182,7 @@ private void maybeCloseFileChannel(@Nullable Throwable cause, boolean onError) { completionFuture.complete(null); } else { if (!onError) { + assert subscription != null; subscription.cancel(); } completionFuture.completeExceptionally(cause); diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/AsyncMapStreamMessage.java b/core/src/main/java/com/linecorp/armeria/common/stream/AsyncMapStreamMessage.java index 96560d42425..d18edf951e2 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/AsyncMapStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/AsyncMapStreamMessage.java @@ -156,7 +156,11 @@ public void onNext(T item) { }); } catch (Throwable ex) { StreamMessageUtil.closeOrAbort(item, ex); + + final Subscription upstream = this.upstream; + assert upstream != null; upstream.cancel(); + onError(ex); } } @@ -170,6 +174,9 @@ private void publishDownstream(@Nullable U item, @Nullable Throwable cause) { return; } + final Subscription upstream = this.upstream; + assert upstream != null; + try { if (cause != null) { upstream.cancel(); @@ -195,7 +202,9 @@ private void publishDownstream(@Nullable U item, @Nullable Throwable cause) { } } } catch (Throwable ex) { - StreamMessageUtil.closeOrAbort(item, ex); + if (item != null) { + StreamMessageUtil.closeOrAbort(item, ex); + } upstream.cancel(); onError(ex); } @@ -230,6 +239,8 @@ public void request(long n) { if (n <= 0) { onError(new IllegalArgumentException( "n: " + n + " (expected: > 0, see Reactive Streams specification rule 3.9)")); + final Subscription upstream = this.upstream; + assert upstream != null; upstream.cancel(); return; } @@ -256,6 +267,8 @@ private void handleRequest(long n) { } requestedFromUpstream = IntMath.saturatedAdd(requestedFromUpstream, (int) toRequest); + final Subscription upstream = this.upstream; + assert upstream != null; upstream.request(toRequest); } } @@ -267,6 +280,8 @@ public void cancel() { } canceled = true; + final Subscription upstream = this.upstream; + assert upstream != null; upstream.cancel(); } } diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/ConcatArrayStreamMessage.java b/core/src/main/java/com/linecorp/armeria/common/stream/ConcatArrayStreamMessage.java index c90cb102e47..b4a64ad2f5a 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/ConcatArrayStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/ConcatArrayStreamMessage.java @@ -115,7 +115,10 @@ public void subscribe(Subscriber subscriber, EventExecutor executor, if (executor.inEventLoop()) { parent.nextSource(); } else { - executor.execute(() -> parent.nextSource()); + executor.execute(() -> { + assert parent != null; + parent.nextSource(); + }); } } else { subscriber.onSubscribe(NoopSubscription.get()); diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/ConcatPublisherStreamMessage.java b/core/src/main/java/com/linecorp/armeria/common/stream/ConcatPublisherStreamMessage.java index e006e08421f..15d46071389 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/ConcatPublisherStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/ConcatPublisherStreamMessage.java @@ -186,6 +186,8 @@ public void onComplete() { } void nextSource() { + final Subscription upstream = this.upstream; + assert upstream != null; upstream.request(1); } } @@ -235,6 +237,7 @@ public void onSubscribe(Subscription subscription) { // Reset 'completed' to subscribe to new Publisher currentPublisherCompleted = false; setUpstreamSubscription(subscription); + assert outerSubscriber != null; outerSubscriber.inInnerOnSubscribe = false; } @@ -270,6 +273,7 @@ public void onComplete() { return; } currentPublisherCompleted = true; + assert outerSubscriber != null; if (outerSubscriber.completed) { downstream.onComplete(); } else { diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/DefaultByteStreamMessage.java b/core/src/main/java/com/linecorp/armeria/common/stream/DefaultByteStreamMessage.java index 5ed833dc669..9a5491088e8 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/DefaultByteStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/DefaultByteStreamMessage.java @@ -159,6 +159,7 @@ public void onNext(HttpData data) { if (position >= end) { // Drop tail bytes. Fully received the desired data already. data.close(); + assert upstream != null; upstream.cancel(); return; } @@ -196,6 +197,7 @@ public void onNext(HttpData data) { } assert position <= end; + assert upstream != null; if (position == end) { onComplete(); upstream.cancel(); @@ -207,6 +209,7 @@ public void onNext(HttpData data) { } private void requestOneOrCancel() { + assert upstream != null; if (position < end) { upstream.request(1); } else { @@ -249,6 +252,7 @@ public void request(long n) { } private void request0(long n) { + assert upstream != null; if (n <= 0) { onError(new IllegalArgumentException( "n: " + n + " (expected: > 0, see Reactive Streams specification rule 3.9)")); @@ -281,6 +285,7 @@ private void cancel0() { return; } completed = true; + assert upstream != null; upstream.cancel(); } } diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/DefaultStreamMessage.java b/core/src/main/java/com/linecorp/armeria/common/stream/DefaultStreamMessage.java index 60e9d78c4bf..856e593c18c 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/DefaultStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/DefaultStreamMessage.java @@ -365,6 +365,7 @@ private void notifySubscriber0() { return; } + assert subscription != null; for (;;) { if (state == State.CLEANUP) { cleanupObjects(null); diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/DeferredStreamMessage.java b/core/src/main/java/com/linecorp/armeria/common/stream/DeferredStreamMessage.java index 1e8e2d74936..350608f2531 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/DeferredStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/DeferredStreamMessage.java @@ -177,6 +177,8 @@ protected final void delegate(StreamMessage upstream) { } if (!collectingFutureUpdater.compareAndSet(this, null, NO_COLLECTING_FUTURE)) { + assert collectingExecutor != null; + assert collectionOptions != null; upstream.collect(collectingExecutor, collectionOptions).handle((result, cause) -> { final CompletableFuture> collectingFuture = this.collectingFuture; assert collectingFuture != null; @@ -296,6 +298,8 @@ private void doCancel() { // Clear the subscriber when we become sure that the upstream will not produce events anymore. final StreamMessage upstream = this.upstream; assert upstream != null; + final SubscriptionImpl downstreamSubscription = this.downstreamSubscription; + assert downstreamSubscription != null; if (upstream.isComplete()) { downstreamSubscription.clearSubscriber(); } else { @@ -452,6 +456,8 @@ public CompletableFuture> collect(EventExecutor executor, SubscriptionOp requireNonNull(options, "options"); if (!downstreamSubscriptionUpdater.compareAndSet(this, null, NOOP_SUBSCRIPTION)) { + final SubscriptionImpl downstreamSubscription = this.downstreamSubscription; + assert downstreamSubscription != null; final Subscriber subscriber = downstreamSubscription.subscriber(); final Throwable cause = abortedOrLate(subscriber); final CompletableFuture> collectingFuture = new CompletableFuture<>(); diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/FuseableStreamMessage.java b/core/src/main/java/com/linecorp/armeria/common/stream/FuseableStreamMessage.java index 393fe4a2bb7..0ddbdc61d4a 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/FuseableStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/FuseableStreamMessage.java @@ -272,6 +272,9 @@ public void onNext(Object item) { return; } + final Subscription upstream = this.upstream; + assert upstream != null; + U result = null; try { if (function != null) { @@ -332,6 +335,8 @@ public void request(long n) { if (canceled) { return; } + final Subscription upstream = this.upstream; + assert upstream != null; upstream.request(n); } @@ -342,6 +347,8 @@ public void cancel() { } canceled = true; + final Subscription upstream = this.upstream; + assert upstream != null; upstream.cancel(); } } diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/PublisherBasedStreamMessage.java b/core/src/main/java/com/linecorp/armeria/common/stream/PublisherBasedStreamMessage.java index 24c0757c4e2..20b568eff55 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/PublisherBasedStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/PublisherBasedStreamMessage.java @@ -170,7 +170,9 @@ private void abort0(Throwable cause) { ImmediateEventExecutor.INSTANCE, false, false); if (!subscriberUpdater.compareAndSet(this, null, abortable)) { - this.subscriber.abort(cause); + final AbortableSubscriber oldSubscriber = this.subscriber; + assert oldSubscriber != null; + oldSubscriber.abort(cause); return; } @@ -289,6 +291,8 @@ private void cancelOrAbort0(boolean cancel) { logger.warn("Subscriber.onError() should not raise an exception. subscriber: {}", subscriber, composite); } finally { + final Subscription subscription = this.subscription; + assert subscription != null; subscription.cancel(); } } diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/StreamMessageCollector.java b/core/src/main/java/com/linecorp/armeria/common/stream/StreamMessageCollector.java index 7baf67a9808..fa8cbc0ce80 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/StreamMessageCollector.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/StreamMessageCollector.java @@ -56,6 +56,7 @@ public void onSubscribe(Subscription s) { public void onNext(T o) { requireNonNull(o, "o"); + assert elementsBuilder != null; elementsBuilder.add(touchOrCopyAndClose(o, withPooledObjects)); } @@ -64,6 +65,7 @@ public void onComplete() { if (future.isDone()) { return; } + assert elementsBuilder != null; future.complete(elementsBuilder.build()); elementsBuilder = null; } @@ -73,6 +75,7 @@ public void onError(Throwable t) { if (future.isDone()) { return; } + assert elementsBuilder != null; final ImmutableList elements = elementsBuilder.build(); for (T element : elements) { StreamMessageUtil.closeOrAbort(element, t); diff --git a/core/src/main/java/com/linecorp/armeria/common/stream/StreamMessageInputStream.java b/core/src/main/java/com/linecorp/armeria/common/stream/StreamMessageInputStream.java index 59bd4d7dcfa..9c7b114a19f 100644 --- a/core/src/main/java/com/linecorp/armeria/common/stream/StreamMessageInputStream.java +++ b/core/src/main/java/com/linecorp/armeria/common/stream/StreamMessageInputStream.java @@ -145,6 +145,8 @@ public void onNext(T item) { byteBufsInputStream.add(result.byteBuf()); } catch (Throwable ex) { StreamMessageUtil.closeOrAbort(item, ex); + final Subscription upstream = this.upstream; + assert upstream != null; upstream.cancel(); onError(ex); } @@ -164,6 +166,8 @@ public void request() { if (byteBufsInputStream.isEos()) { return; } + final Subscription upstream = this.upstream; + assert upstream != null; upstream.request(1); } } diff --git a/core/src/main/java/com/linecorp/armeria/common/util/AbstractUnwrappable.java b/core/src/main/java/com/linecorp/armeria/common/util/AbstractUnwrappable.java index a5cfb6fe02c..d1ccb61751e 100644 --- a/core/src/main/java/com/linecorp/armeria/common/util/AbstractUnwrappable.java +++ b/core/src/main/java/com/linecorp/armeria/common/util/AbstractUnwrappable.java @@ -17,6 +17,8 @@ import static java.util.Objects.requireNonNull; +import com.linecorp.armeria.common.annotation.Nullable; + /** * Skeletal {@link Unwrappable} implementation. * @@ -33,6 +35,7 @@ protected AbstractUnwrappable(T delegate) { this.delegate = requireNonNull(delegate, "delegate"); } + @Nullable @Override public final U as(Class type) { final U result = Unwrappable.super.as(type); diff --git a/core/src/main/java/com/linecorp/armeria/common/util/AppRootFinder.java b/core/src/main/java/com/linecorp/armeria/common/util/AppRootFinder.java index 194999de528..51de02fa1e8 100644 --- a/core/src/main/java/com/linecorp/armeria/common/util/AppRootFinder.java +++ b/core/src/main/java/com/linecorp/armeria/common/util/AppRootFinder.java @@ -60,6 +60,7 @@ public static Path findCurrent(int callDepth) { // - This class // - The anonymous SecurityManager final Class[] classes = classContextRef.get(); + assert classes != null; final int toSkip = 2; if (callDepth < 0 || callDepth + toSkip >= classes.length) { diff --git a/core/src/main/java/com/linecorp/armeria/common/util/Exceptions.java b/core/src/main/java/com/linecorp/armeria/common/util/Exceptions.java index c20d52b761c..8839cff07a2 100644 --- a/core/src/main/java/com/linecorp/armeria/common/util/Exceptions.java +++ b/core/src/main/java/com/linecorp/armeria/common/util/Exceptions.java @@ -173,6 +173,7 @@ public static boolean isStreamCancelling(Throwable cause) { requireNonNull(cause, "cause"); if (cause instanceof UnprocessedRequestException) { cause = cause.getCause(); + assert cause != null; } for (ExceptionClassifier classifier : exceptionClassifiers) { @@ -218,7 +219,7 @@ public static T clearTrace(T exception) { * e.g. {@code return Exceptions.throwUnsafely(...);} vs. * {@code Exceptions.throwUnsafely(...); return null;} */ - @SuppressWarnings("ReturnOfNull") + @SuppressWarnings({ "ReturnOfNull", "NullAway" }) public static T throwUnsafely(Throwable cause) { doThrowUnsafely(requireNonNull(cause, "cause")); return null; // Never reaches here. diff --git a/core/src/main/java/com/linecorp/armeria/common/util/Version.java b/core/src/main/java/com/linecorp/armeria/common/util/Version.java index 230610e3b83..3b20e6c3a0c 100644 --- a/core/src/main/java/com/linecorp/armeria/common/util/Version.java +++ b/core/src/main/java/com/linecorp/armeria/common/util/Version.java @@ -163,7 +163,7 @@ public static Map getAll(ClassLoader classLoader) { for (Object o : props.keySet()) { final String k = (String) o; - final int dotIndex = k.indexOf('.'); + final int dotIndex = k.lastIndexOf('.'); if (dotIndex <= 0) { continue; } diff --git a/core/src/main/java/com/linecorp/armeria/common/websocket/CloseByteBufWebSocketFrame.java b/core/src/main/java/com/linecorp/armeria/common/websocket/CloseByteBufWebSocketFrame.java index 5aa2345ae98..bb2244a9e9d 100644 --- a/core/src/main/java/com/linecorp/armeria/common/websocket/CloseByteBufWebSocketFrame.java +++ b/core/src/main/java/com/linecorp/armeria/common/websocket/CloseByteBufWebSocketFrame.java @@ -86,6 +86,7 @@ private static int validateStatusCode(int statusCode) { } } + @Nullable @Override public String reasonPhrase() { return reasonPhrase; @@ -135,7 +136,7 @@ public boolean equals(Object obj) { final CloseByteBufWebSocketFrame that = (CloseByteBufWebSocketFrame) obj; return status.equals(that.status()) && - reasonPhrase.equals(that.reasonPhrase()) && + Objects.equals(reasonPhrase, that.reasonPhrase()) && super.equals(obj); } diff --git a/core/src/main/java/com/linecorp/armeria/internal/client/DefaultClientRequestContext.java b/core/src/main/java/com/linecorp/armeria/internal/client/DefaultClientRequestContext.java index f6b0478c4bb..60632969248 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/client/DefaultClientRequestContext.java +++ b/core/src/main/java/com/linecorp/armeria/internal/client/DefaultClientRequestContext.java @@ -416,7 +416,9 @@ public CompletableFuture whenInitialized() { if (whenInitializedUpdater.compareAndSet(this, null, whenInitialized)) { return whenInitialized; } else { - return this.whenInitialized; + final CompletableFuture oldWhenInitialized = this.whenInitialized; + assert oldWhenInitialized != null; + return oldWhenInitialized; } } } @@ -429,7 +431,9 @@ public void finishInitialization(boolean success) { } else { if (!whenInitializedUpdater.compareAndSet(this, null, UnmodifiableFuture.completedFuture(success))) { - this.whenInitialized.complete(success); + final CompletableFuture oldWhenInitialized = this.whenInitialized; + assert oldWhenInitialized != null; + oldWhenInitialized.complete(success); } } } @@ -621,6 +625,7 @@ public ClientRequestContext newDerivedContext(RequestId id, sessionProtocol(), method(), requestTarget()); } + @Nullable @Override protected RequestTarget validateHeaders(RequestHeaders headers) { // no need to validate since internal headers will contain @@ -645,6 +650,7 @@ protected Channel channel() { return newChannel; } + @Nullable @Override public InetSocketAddress remoteAddress() { final InetSocketAddress remoteAddress = this.remoteAddress; @@ -657,6 +663,7 @@ public InetSocketAddress remoteAddress() { return newRemoteAddress; } + @Nullable @Override public InetSocketAddress localAddress() { final InetSocketAddress localAddress = this.localAddress; @@ -696,11 +703,13 @@ public ClientOptions options() { return options; } + @Nullable @Override public EndpointGroup endpointGroup() { return endpointGroup; } + @Nullable @Override public Endpoint endpoint() { return endpoint; @@ -712,6 +721,7 @@ public String fragment() { return requestTarget().fragment(); } + @Nullable @Override public String authority() { final HttpHeaders additionalRequestHeaders = this.additionalRequestHeaders; @@ -755,6 +765,7 @@ private String origin() { return origin; } + @Nullable @Override public String host() { final String authority = authority(); diff --git a/core/src/main/java/com/linecorp/armeria/internal/client/PublicSuffix.java b/core/src/main/java/com/linecorp/armeria/internal/client/PublicSuffix.java index b5fbdb62a0d..15bff2a9fea 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/client/PublicSuffix.java +++ b/core/src/main/java/com/linecorp/armeria/internal/client/PublicSuffix.java @@ -100,7 +100,7 @@ private void buildTrie() { for (int i = labels.length - 1; i >= 0; i--) { // assume wildcard is at the first position, we don't need to create a new node for it // but set the current node's isWildcard = true instead - if (labels[i].equals("*")) { + if ("*".equals(labels[i])) { node.isWildcard = true; break; } @@ -109,6 +109,7 @@ private void buildTrie() { } if (node.children.containsKey(labels[i])) { node = node.children.get(labels[i]); + assert node != null; } else { final TrieNode newNode = new TrieNode(); if (labels[i].charAt(0) == '!') { @@ -157,12 +158,20 @@ public boolean isPublicSuffix(String domain) { for (int i = end; i >= start; i--) { if (node.children != null && node.children.containsKey(labels[i])) { node = node.children.get(labels[i]); + assert node != null; } else { return false; } if (i == 1 && node.isWildcard) { - return node.children == null || !node.children.containsKey(labels[0]) || - !node.children.get(labels[0]).isException; + if (node.children == null) { + return true; + } + if (!node.children.containsKey(labels[0])) { + return true; + } + final TrieNode child = node.children.get(labels[0]); + assert child != null; + return !child.isException; } } return node.isEnd; diff --git a/core/src/main/java/com/linecorp/armeria/internal/client/dns/DnsUtil.java b/core/src/main/java/com/linecorp/armeria/internal/client/dns/DnsUtil.java index d892be478d9..d7f5528f457 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/client/dns/DnsUtil.java +++ b/core/src/main/java/com/linecorp/armeria/internal/client/dns/DnsUtil.java @@ -131,7 +131,10 @@ public static long defaultDnsQueryTimeoutMillis() { return DEFAULT_DNS_QUERY_TIMEOUT_MILLIS; } - public static boolean isDnsQueryTimedOut(Throwable cause) { + public static boolean isDnsQueryTimedOut(@Nullable Throwable cause) { + if (cause == null) { + return false; + } final Throwable rootCause = Throwables.getRootCause(cause); if (rootCause instanceof DnsErrorCauseException) { return false; diff --git a/core/src/main/java/com/linecorp/armeria/internal/client/endpoint/StaticEndpointGroup.java b/core/src/main/java/com/linecorp/armeria/internal/client/endpoint/StaticEndpointGroup.java index 0d4d7046512..af0a3cf6248 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/client/endpoint/StaticEndpointGroup.java +++ b/core/src/main/java/com/linecorp/armeria/internal/client/endpoint/StaticEndpointGroup.java @@ -71,6 +71,7 @@ public EndpointSelectionStrategy selectionStrategy() { return selectionStrategy; } + @Nullable @Override public Endpoint selectNow(ClientRequestContext ctx) { return selector.selectNow(ctx); diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/BuiltInDependencyInjector.java b/core/src/main/java/com/linecorp/armeria/internal/common/BuiltInDependencyInjector.java index 35fc3667b31..bb89da96605 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/BuiltInDependencyInjector.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/BuiltInDependencyInjector.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableSet; import com.linecorp.armeria.common.DependencyInjector; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.server.annotation.ServerSentEventResponseConverterFunction; import com.linecorp.armeria.server.annotation.decorator.LoggingDecoratorFactoryFunction; import com.linecorp.armeria.server.annotation.decorator.RateLimitingDecoratorFactoryFunction; @@ -42,6 +43,7 @@ public enum BuiltInDependencyInjector implements DependencyInjector { private static final Map, Object> instances = new ConcurrentHashMap<>(); + @Nullable @Override public T getInstance(Class type) { if (!builtInClasses.contains(type)) { diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/DefaultCancellationScheduler.java b/core/src/main/java/com/linecorp/armeria/internal/common/DefaultCancellationScheduler.java index 536b3e8a0bc..df69d01ac38 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/DefaultCancellationScheduler.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/DefaultCancellationScheduler.java @@ -171,6 +171,7 @@ public void clearTimeout(boolean resetTimeout) { return; } if (isInitialized()) { + assert eventLoop != null; if (eventLoop.inEventLoop()) { clearTimeout0(resetTimeout); } else { @@ -223,6 +224,7 @@ private void setTimeoutNanosFromStart(long timeoutNanos) { return; } if (isInitialized()) { + assert eventLoop != null; if (eventLoop.inEventLoop()) { setTimeoutNanosFromStart0(timeoutNanos); } else { @@ -254,6 +256,7 @@ private void extendTimeoutNanos(long adjustmentNanos) { return; } if (isInitialized()) { + assert eventLoop != null; if (eventLoop.inEventLoop()) { extendTimeoutNanos0(adjustmentNanos); } else { @@ -285,6 +288,7 @@ private void extendTimeoutNanos0(long adjustmentNanos) { private void setTimeoutNanosFromNow(long timeoutNanos) { checkArgument(timeoutNanos > 0, "timeoutNanos: %s (expected: > 0)", timeoutNanos); if (isInitialized()) { + assert eventLoop != null; if (eventLoop.inEventLoop()) { setTimeoutNanosFromNow0(timeoutNanos); } else { @@ -393,7 +397,9 @@ public CompletableFuture whenCancelling() { if (whenCancellingUpdater.compareAndSet(this, null, cancellationFuture)) { return cancellationFuture; } else { - return this.whenCancelling; + final CancellationFuture oldWhenCancelling = this.whenCancelling; + assert oldWhenCancelling != null; + return oldWhenCancelling; } } @@ -407,7 +413,9 @@ public CompletableFuture whenCancelled() { if (whenCancelledUpdater.compareAndSet(this, null, cancellationFuture)) { return cancellationFuture; } else { - return this.whenCancelled; + final CancellationFuture oldWhenCancelled = this.whenCancelled; + assert oldWhenCancelled != null; + return oldWhenCancelled; } } @@ -427,7 +435,9 @@ public CompletableFuture whenTimingOut() { }); return timeoutFuture; } else { - return this.whenTimingOut; + final TimeoutFuture oldWhenTimingOut = this.whenTimingOut; + assert oldWhenTimingOut != null; + return oldWhenTimingOut; } } @@ -447,7 +457,9 @@ public CompletableFuture whenTimedOut() { }); return timeoutFuture; } else { - return this.whenTimedOut; + final TimeoutFuture oldWhenTimedOut = this.whenTimedOut; + assert oldWhenTimedOut != null; + return oldWhenTimedOut; } } diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java b/core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java index 0bee70b8bb6..47d0fd0800f 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java @@ -235,11 +235,13 @@ public RequestTargetForm form() { return form; } + @Nullable @Override public String scheme() { return scheme; } + @Nullable @Override public String authority() { return authority; @@ -266,11 +268,13 @@ public String maybePathWithMatrixVariables() { return maybePathWithMatrixVariables; } + @Nullable @Override public String query() { return query; } + @Nullable @Override public String fragment() { return fragment; diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/HttpObjectAggregator.java b/core/src/main/java/com/linecorp/armeria/internal/common/HttpObjectAggregator.java index f10e63ddf04..47b3cd789b9 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/HttpObjectAggregator.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/HttpObjectAggregator.java @@ -121,6 +121,7 @@ protected void onData(HttpData data) { if (dataLength > 0) { final int allowedMaxDataLength = Integer.MAX_VALUE - contentLength; if (dataLength > allowedMaxDataLength) { + assert subscription != null; subscription.cancel(); fail(new IllegalStateException("content length greater than Integer.MAX_VALUE")); return; diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/NonWrappingRequestContext.java b/core/src/main/java/com/linecorp/armeria/internal/common/NonWrappingRequestContext.java index 82665f30b03..08d38b39579 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/NonWrappingRequestContext.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/NonWrappingRequestContext.java @@ -109,11 +109,13 @@ protected NonWrappingRequestContext( this.contextHook = (Supplier) contextHook; } + @Nullable @Override public final HttpRequest request() { return req; } + @Nullable @Override public final RpcRequest rpcRequest() { return rpcReq; @@ -192,6 +194,7 @@ public final String decodedPath() { return this.decodedPath = ArmeriaHttpUtil.decodePath(path()); } + @Nullable @Override public final String query() { return reqTarget.query(); @@ -231,6 +234,7 @@ public final V ownAttr(AttributeKey key) { return attrs.ownAttr(key); } + @Nullable @Override public final V setAttr(AttributeKey key, @Nullable V value) { requireNonNull(key, "key"); diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/ReflectiveDependencyInjector.java b/core/src/main/java/com/linecorp/armeria/internal/common/ReflectiveDependencyInjector.java index 939405d82b5..53143e1cb70 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/ReflectiveDependencyInjector.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/ReflectiveDependencyInjector.java @@ -64,6 +64,7 @@ public static T create(Class type, @Nullable Map, Obje private boolean isShutdown; + @Nullable @Override public T getInstance(Class type) { lock.lock(); diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/SplitHttpMessageSubscriber.java b/core/src/main/java/com/linecorp/armeria/internal/common/SplitHttpMessageSubscriber.java index 5bec18b29e5..865f39523df 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/SplitHttpMessageSubscriber.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/SplitHttpMessageSubscriber.java @@ -105,7 +105,9 @@ final CompletableFuture trailersFuture() { if (trailersFutureUpdater.compareAndSet(this, null, trailersFuture)) { return trailersFuture; } else { - return this.trailersFuture; + final HeadersFuture oldTrailersFuture = this.trailersFuture; + assert oldTrailersFuture != null; + return oldTrailersFuture; } } @@ -291,7 +293,9 @@ private void completeTrailers(HttpHeaders trailers) { if (trailersFutureUpdater.compareAndSet(this, null, trailersFuture)) { trailersFuture.doComplete(trailers); } else { - this.trailersFuture.doComplete(trailers); + final HeadersFuture oldTrailersFuture = this.trailersFuture; + assert oldTrailersFuture != null; + oldTrailersFuture.doComplete(trailers); } } diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/encoding/DefaultHttpDecodedResponse.java b/core/src/main/java/com/linecorp/armeria/internal/common/encoding/DefaultHttpDecodedResponse.java index ad6ee225d0c..fa2d2c095a7 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/encoding/DefaultHttpDecodedResponse.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/encoding/DefaultHttpDecodedResponse.java @@ -115,6 +115,7 @@ protected HttpObject filter(HttpObject obj) { return decoder != null ? decoder.decode((HttpData) obj) : obj; } + @Nullable @Override StreamDecoder decoder() { return decoder; diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/stream/DecodedStreamMessage.java b/core/src/main/java/com/linecorp/armeria/internal/common/stream/DecodedStreamMessage.java index a7da38a221a..f2585f59170 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/stream/DecodedStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/stream/DecodedStreamMessage.java @@ -127,6 +127,7 @@ private void initialize() { initialized = true; if (cancelled) { + assert upstream != null; upstream.cancel(); return; } diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/stream/FixedStreamMessage.java b/core/src/main/java/com/linecorp/armeria/internal/common/stream/FixedStreamMessage.java index c5dfadbe06e..d274ac2f8db 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/stream/FixedStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/stream/FixedStreamMessage.java @@ -267,6 +267,7 @@ void onError(Throwable cause) { private void onError0(Throwable cause) { try { + assert subscriber != null; subscriber.onError(cause); if (!completionFuture.isDone()) { completionFuture.completeExceptionally(cause); diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/stream/ThreeElementFixedStreamMessage.java b/core/src/main/java/com/linecorp/armeria/internal/common/stream/ThreeElementFixedStreamMessage.java index 3b793b3a306..eb7e6064a6e 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/stream/ThreeElementFixedStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/stream/ThreeElementFixedStreamMessage.java @@ -76,6 +76,8 @@ final void cleanupObjects(@Nullable Throwable cause) { @Override final List drainAll(boolean withPooledObjects) { assert obj1 != null; + assert obj2 != null; + assert obj3 != null; final List objs = ImmutableList.of(touchOrCopyAndClose(obj1, withPooledObjects), touchOrCopyAndClose(obj2, withPooledObjects), touchOrCopyAndClose(obj3, withPooledObjects)); diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/stream/TwoElementFixedStreamMessage.java b/core/src/main/java/com/linecorp/armeria/internal/common/stream/TwoElementFixedStreamMessage.java index a995e7da770..a32acbe4e6a 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/stream/TwoElementFixedStreamMessage.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/stream/TwoElementFixedStreamMessage.java @@ -72,6 +72,7 @@ final void cleanupObjects(@Nullable Throwable cause) { @Override final List drainAll(boolean withPooledObjects) { assert obj1 != null; + assert obj2 != null; final List objs = ImmutableList.of(touchOrCopyAndClose(obj1, withPooledObjects), touchOrCopyAndClose(obj2, withPooledObjects)); obj1 = obj2 = null; diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/util/ChannelUtil.java b/core/src/main/java/com/linecorp/armeria/internal/common/util/ChannelUtil.java index f51fb8d247e..7b47708f5ca 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/util/ChannelUtil.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/util/ChannelUtil.java @@ -115,6 +115,11 @@ public final class ChannelUtil { static { final ImmutableSet.Builder> tcpOptionsBuilder = ImmutableSet.builder(); + + ChannelOption epollTcpUserTimeout = null; + ChannelOption epollTcpKeepidle = null; + ChannelOption epollTcpKeepintvl = null; + try { final Class clazz = Class.forName( CHANNEL_PACKAGE_NAME + ".epoll.EpollChannelOption", false, @@ -126,14 +131,28 @@ public final class ChannelUtil { //noinspection unchecked epollTcpKeepintvl = (ChannelOption) findChannelOption(clazz, "TCP_KEEPINTVL"); - tcpOptionsBuilder.add(epollTcpUserTimeout); - tcpOptionsBuilder.add(epollTcpKeepidle); - tcpOptionsBuilder.add(epollTcpKeepintvl); + if (epollTcpUserTimeout != null) { + tcpOptionsBuilder.add(epollTcpUserTimeout); + } + if (epollTcpKeepidle != null) { + tcpOptionsBuilder.add(epollTcpKeepidle); + } + if (epollTcpKeepintvl != null) { + tcpOptionsBuilder.add(epollTcpKeepintvl); + } } catch (Throwable ignored) { // Ignore } + ChannelUtil.epollTcpUserTimeout = epollTcpUserTimeout; + ChannelUtil.epollTcpKeepidle = epollTcpKeepidle; + ChannelUtil.epollTcpKeepintvl = epollTcpKeepintvl; + if (INCUBATOR_CHANNEL_PACKAGE_NAME != null) { + ChannelOption ioUringTcpUserTimeout = null; + ChannelOption ioUringTcpKeepidle = null; + ChannelOption ioUringTcpKeepintvl = null; + try { final Class clazz = Class.forName( INCUBATOR_CHANNEL_PACKAGE_NAME + ".uring.IOUringChannelOption", false, @@ -145,19 +164,29 @@ public final class ChannelUtil { //noinspection unchecked ioUringTcpKeepintvl = (ChannelOption) findChannelOption(clazz, "TCP_KEEPINTVL"); - tcpOptionsBuilder.add(ioUringTcpUserTimeout); - tcpOptionsBuilder.add(ioUringTcpKeepidle); - tcpOptionsBuilder.add(ioUringTcpKeepintvl); + if (ioUringTcpUserTimeout != null) { + tcpOptionsBuilder.add(ioUringTcpUserTimeout); + } + if (ioUringTcpKeepidle != null) { + tcpOptionsBuilder.add(ioUringTcpKeepidle); + } + if (ioUringTcpKeepintvl != null) { + tcpOptionsBuilder.add(ioUringTcpKeepintvl); + } } catch (Throwable ignored) { // Ignore } + + ChannelUtil.ioUringTcpUserTimeout = ioUringTcpUserTimeout; + ChannelUtil.ioUringTcpKeepidle = ioUringTcpKeepidle; + ChannelUtil.ioUringTcpKeepintvl = ioUringTcpKeepintvl; } tcpOptions = tcpOptionsBuilder.build(); } @Nullable - private static ChannelOption findChannelOption(Class clazz, String fieldName) throws Throwable { + private static ChannelOption findChannelOption(Class clazz, String fieldName) { try { final MethodHandle methodHandle = MethodHandles.publicLookup().findStaticGetter( clazz, fieldName, ChannelOption.class); @@ -257,9 +286,11 @@ static Map, Object> applyDefaultChannelOptions( final int tcpUserTimeout = Ints.saturatedCast(idleTimeoutMillis + TCP_USER_TIMEOUT_BUFFER_MILLIS); if (transportType == TransportType.EPOLL && canAddChannelOption(epollTcpUserTimeout, channelOptions)) { + assert epollTcpUserTimeout != null; putChannelOption(newChannelOptionsBuilder, epollTcpUserTimeout, tcpUserTimeout); } else if (transportType == TransportType.IO_URING && canAddChannelOption(ioUringTcpUserTimeout, channelOptions)) { + assert ioUringTcpUserTimeout != null; putChannelOption(newChannelOptionsBuilder, ioUringTcpUserTimeout, tcpUserTimeout); } } @@ -270,6 +301,8 @@ static Map, Object> applyDefaultChannelOptions( canAddChannelOption(epollTcpKeepidle, channelOptions) && canAddChannelOption(epollTcpKeepintvl, channelOptions) && canAddChannelOption(ChannelOption.SO_KEEPALIVE, channelOptions)) { + assert epollTcpKeepidle != null; + assert epollTcpKeepintvl != null; putChannelOption(newChannelOptionsBuilder, ChannelOption.SO_KEEPALIVE, true); putChannelOption(newChannelOptionsBuilder, epollTcpKeepidle, intPingIntervalMillis); putChannelOption(newChannelOptionsBuilder, epollTcpKeepintvl, intPingIntervalMillis); @@ -277,6 +310,8 @@ static Map, Object> applyDefaultChannelOptions( canAddChannelOption(ioUringTcpKeepidle, channelOptions) && canAddChannelOption(ioUringTcpKeepintvl, channelOptions) && canAddChannelOption(ChannelOption.SO_KEEPALIVE, channelOptions)) { + assert ioUringTcpKeepidle != null; + assert ioUringTcpKeepintvl != null; putChannelOption(newChannelOptionsBuilder, ChannelOption.SO_KEEPALIVE, true); putChannelOption(newChannelOptionsBuilder, ioUringTcpKeepidle, intPingIntervalMillis); putChannelOption(newChannelOptionsBuilder, ioUringTcpKeepintvl, intPingIntervalMillis); diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/util/MinifiedBouncyCastleProvider.java b/core/src/main/java/com/linecorp/armeria/internal/common/util/MinifiedBouncyCastleProvider.java index 43dafc7c332..4dc27732c00 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/util/MinifiedBouncyCastleProvider.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/util/MinifiedBouncyCastleProvider.java @@ -41,6 +41,8 @@ import com.google.common.annotations.VisibleForTesting; +import com.linecorp.armeria.common.annotation.Nullable; + /** * A downsized version of {@link BouncyCastleProvider} which provides only RSA/DSA/EC {@link KeyFactorySpi}s * and X.509 {@link CertificateFactorySpi}. @@ -159,6 +161,7 @@ public void addKeyInfoConverter(ASN1ObjectIdentifier oid, AsymmetricKeyInfoConve keyInfoConverters.put(oid, keyInfoConverter); } + @Nullable @Override public AsymmetricKeyInfoConverter getKeyInfoConverter(ASN1ObjectIdentifier oid) { return keyInfoConverters.get(oid); diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/util/StringUtil.java b/core/src/main/java/com/linecorp/armeria/internal/common/util/StringUtil.java index f19659b3b31..7a4bcacdee6 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/util/StringUtil.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/util/StringUtil.java @@ -20,6 +20,8 @@ import com.google.common.collect.ImmutableMap; +import com.linecorp.armeria.common.annotation.Nullable; + public final class StringUtil { private static final int MAX_NUM = 1000; private static final int MIN_NUM = -MAX_NUM; @@ -56,15 +58,17 @@ public static String toString(long num) { return Long.toString(num); } - public static Boolean toBoolean(String s, boolean errorOnFailure) { + public static Boolean toBoolean(String s) { final Boolean result = stringToBoolean.get(s); if (result != null) { return result; } - if (errorOnFailure) { - throw new IllegalArgumentException("must be one of " + stringToBoolean.keySet() + ": " + s); - } - return null; + throw new IllegalArgumentException("must be one of " + stringToBoolean.keySet() + ": " + s); + } + + @Nullable + public static Boolean toBooleanOrNull(String s) { + return stringToBoolean.get(s); } private StringUtil() {} diff --git a/core/src/main/java/com/linecorp/armeria/internal/common/util/TransportTypeProvider.java b/core/src/main/java/com/linecorp/armeria/internal/common/util/TransportTypeProvider.java index ca5c939a428..424e8959769 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/common/util/TransportTypeProvider.java +++ b/core/src/main/java/com/linecorp/armeria/internal/common/util/TransportTypeProvider.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.internal.common.util; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.Objects.requireNonNull; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -171,10 +172,19 @@ private static TransportTypeProvider of( final Class sc = findClass(channelPackageName, socketChannelTypeName); - final Class sdsc = - findClass(channelPackageName, domainServerSocketChannelTypeName); - final Class dsc = - findClass(channelPackageName, domainSocketChannelTypeName); + final Class sdsc; + if (domainServerSocketChannelTypeName != null) { + sdsc = findClass(channelPackageName, domainServerSocketChannelTypeName); + } else { + sdsc = null; + } + + final Class dsc; + if (domainSocketChannelTypeName != null) { + dsc = findClass(channelPackageName, domainSocketChannelTypeName); + } else { + dsc = null; + } final Class dc = findClass(channelPackageName, datagramChannelTypeName); @@ -193,20 +203,15 @@ private static TransportTypeProvider of( } } - @Nullable @SuppressWarnings("unchecked") - private static Class findClass(String channelPackageName, - @Nullable String className) throws Exception { - if (className == null) { - return null; - } - + private static Class findClass(String channelPackageName, String className) throws Exception { return (Class) Class.forName(channelPackageName + className, false, TransportTypeProvider.class.getClassLoader()); } private static BiFunction findEventLoopGroupConstructor( Class eventLoopGroupType) throws Exception { + requireNonNull(eventLoopGroupType, "eventLoopGroupType"); final MethodHandle constructor = MethodHandles.lookup().unreflectConstructor( eventLoopGroupType.getConstructor(int.class, ThreadFactory.class)); diff --git a/core/src/main/java/com/linecorp/armeria/internal/server/DefaultServiceRequestContext.java b/core/src/main/java/com/linecorp/armeria/internal/server/DefaultServiceRequestContext.java index fb4dea410dc..b1502d58112 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/server/DefaultServiceRequestContext.java +++ b/core/src/main/java/com/linecorp/armeria/internal/server/DefaultServiceRequestContext.java @@ -210,6 +210,7 @@ public DefaultServiceRequestContext( this.additionalResponseTrailers = additionalResponseTrailers; } + @Nullable @Override protected RequestTarget validateHeaders(RequestHeaders headers) { checkArgument(headers.scheme() != null && headers.authority() != null, diff --git a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AggregatedResponseConverterFunction.java b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AggregatedResponseConverterFunction.java index 1d0ddfcddec..269b4d1a919 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AggregatedResponseConverterFunction.java +++ b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AggregatedResponseConverterFunction.java @@ -47,6 +47,7 @@ final class AggregatedResponseConverterFunction implements ResponseConverterFunc this.responseConverter = responseConverter; } + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType contentType) { final Class clazz = typeToClass(unwrapUnaryAsyncType(returnType)); diff --git a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePlugin.java b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePlugin.java index 8639234a4e9..7468e258c1d 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePlugin.java +++ b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePlugin.java @@ -270,6 +270,7 @@ private static FieldInfo fieldInfo(AnnotatedValueResolver resolver) { return null; } + assert beanFactoryId != null; final Class type = beanFactoryId.type(); typeSignature = new RequestObjectTypeSignature(TypeSignatureType.STRUCT, type.getName(), type, new AnnotatedValueResolversWrapper(resolvers)); diff --git a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedElementNameUtil.java b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedElementNameUtil.java index 8029ecbb787..29dceed06b6 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedElementNameUtil.java +++ b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedElementNameUtil.java @@ -31,9 +31,9 @@ final class AnnotatedElementNameUtil { /** - * Returns the value of {@link Header}, {@link Param}, {@link Attribute} if the value is not blank. + * Returns the value of {@link Param}, {@link Attribute} if the value is not blank. * If the value is blank, it returns the name of the specified {@code nameRetrievalTarget} object - * which is an instance of {@link Header}, {@link Param}, {@link Attribute} or {@link Field}. + * which is an instance of {@link Param}, {@link Attribute} or {@link Field}. */ static String findName(Object nameRetrievalTarget, String value) { requireNonNull(nameRetrievalTarget, "nameRetrievalTarget"); @@ -44,6 +44,27 @@ static String findName(Object nameRetrievalTarget, String value) { return getName(nameRetrievalTarget); } + /** + * Returns the value of the {@link Header} annotation which is specified on the {@code element} if + * the value is not blank. If the value is blank, it returns the name of the specified + * {@code nameRetrievalTarget} object which is an instance of {@link Parameter} or {@link Field}. + * + *

Note that the name of the specified {@code nameRetrievalTarget} will be converted as + * {@link CaseFormat#LOWER_HYPHEN} that the string elements are separated with one hyphen({@code -}) + * character. The value of the {@link Header} annotation will not be converted because it is clearly + * specified by a user. + */ + static String findName(Header header, Object nameRetrievalTarget) { + requireNonNull(nameRetrievalTarget, "nameRetrievalTarget"); + + final String value = header.value(); + if (DefaultValues.isSpecified(value)) { + checkArgument(!value.isEmpty(), "value is empty."); + return value; + } + return toHeaderName(getName(nameRetrievalTarget)); + } + /** * Returns the name of the specified element or the default name if it can't get. */ diff --git a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTypeUtil.java b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTypeUtil.java index 1172607c9f5..b59c015ee46 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTypeUtil.java +++ b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTypeUtil.java @@ -55,8 +55,8 @@ final class AnnotatedServiceTypeUtil { .put(Byte.class, Byte::valueOf) .put(Short.TYPE, Short::valueOf) .put(Short.class, Short::valueOf) - .put(Boolean.TYPE, s -> StringUtil.toBoolean(s, true)) - .put(Boolean.class, s -> StringUtil.toBoolean(s, true)) + .put(Boolean.TYPE, s -> StringUtil.toBoolean(s)) + .put(Boolean.class, s -> StringUtil.toBoolean(s)) .put(Integer.TYPE, Integer::valueOf) .put(Integer.class, Integer::valueOf) .put(Long.TYPE, Long::valueOf) @@ -154,7 +154,9 @@ static Function getCreatorMethod(Class clazz) { try { return (T) methodHandle.invokeWithArguments(str); } catch (InvocationTargetException e) { - return Exceptions.throwUnsafely(e.getCause()); + final Throwable cause = e.getCause(); + assert cause != null; + return Exceptions.throwUnsafely(cause); } catch (Throwable t) { return Exceptions.throwUnsafely(t); } diff --git a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedValueResolver.java b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedValueResolver.java index e782a09bf89..546c4f443a0 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedValueResolver.java +++ b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedValueResolver.java @@ -22,7 +22,6 @@ import static com.linecorp.armeria.internal.server.annotation.AnnotatedElementNameUtil.findName; import static com.linecorp.armeria.internal.server.annotation.AnnotatedElementNameUtil.getName; import static com.linecorp.armeria.internal.server.annotation.AnnotatedElementNameUtil.getNameOrDefault; -import static com.linecorp.armeria.internal.server.annotation.AnnotatedElementNameUtil.toHeaderName; import static com.linecorp.armeria.internal.server.annotation.AnnotatedServiceFactory.findDescription; import static com.linecorp.armeria.internal.server.annotation.AnnotatedServiceTypeUtil.stringToType; import static com.linecorp.armeria.internal.server.annotation.DefaultValues.getSpecifiedValue; @@ -471,7 +470,7 @@ private static AnnotatedValueResolver of(AnnotatedElement annotatedElement, final Header header = annotatedElement.getAnnotation(Header.class); if (header != null) { - final String name = toHeaderName(findName(typeElement, header.value())); + final String name = findName(header, typeElement); return ofHeader(name, annotatedElement, typeElement, type, description); } @@ -727,7 +726,9 @@ private static AnnotatedValueResolver ofInjectableTypes0(String name, AnnotatedE return new Builder(annotatedElement, type, name) .resolver((unused, ctx) -> { final String filename = getName(annotatedElement); - return Iterables.getFirst(ctx.aggregatedMultipart().files().get(filename), null); + final FileAggregatedMultipart aggregatedMultipart = ctx.aggregatedMultipart(); + assert aggregatedMultipart != null; + return Iterables.getFirst(aggregatedMultipart.files().get(filename), null); }) .aggregation(AggregationStrategy.ALWAYS) .build(); @@ -1597,6 +1598,7 @@ static class ResolverContext { @Nullable private final AggregatedResult aggregatedResult; + @Nullable private volatile QueryParams queryParams; ResolverContext(ServiceRequestContext context, HttpRequest request, @@ -1616,11 +1618,13 @@ HttpRequest request() { @Nullable AggregatedHttpRequest aggregatedRequest() { + assert aggregatedResult != null; return aggregatedResult.aggregatedHttpRequest; } @Nullable FileAggregatedMultipart aggregatedMultipart() { + assert aggregatedResult != null; return aggregatedResult.aggregatedMultipart; } @@ -1629,6 +1633,7 @@ QueryParams queryParams() { if (result == null) { result = queryParams; if (result == null) { + assert aggregatedResult != null; queryParams = result = queryParamsOf(context.query(), request.contentType(), aggregatedResult); @@ -1665,6 +1670,7 @@ private static QueryParams queryParamsOf(@Nullable String query, final QueryParams params1 = query != null ? QueryParams.fromQueryString(query) : null; QueryParams params2 = null; if (isFormData(contentType)) { + assert contentType != null; final AggregatedHttpRequest message = result.aggregatedHttpRequest; if (message != null) { // Respect 'charset' attribute of the 'content-type' header if it exists. diff --git a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/DefaultAnnotatedService.java b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/DefaultAnnotatedService.java index 24143652354..3d3d3d2e534 100644 --- a/core/src/main/java/com/linecorp/armeria/internal/server/annotation/DefaultAnnotatedService.java +++ b/core/src/main/java/com/linecorp/armeria/internal/server/annotation/DefaultAnnotatedService.java @@ -250,6 +250,7 @@ private static ServiceOptions buildServiceOptions(ServiceOption serviceOption) { return builder.build(); } + @Nullable @Override public String name() { return name; @@ -328,7 +329,9 @@ private HttpResponse serve0(ServiceRequestContext ctx, HttpRequest req) { // Fast-path: No aggregation required and blocking task executor is not used. switch (responseType) { case HTTP_RESPONSE: - return (HttpResponse) invoke(ctx, req, AggregatedResult.EMPTY); + final HttpResponse res = (HttpResponse) invoke(ctx, req, AggregatedResult.EMPTY); + assert res != null; + return res; case OTHER_OBJECTS: return convertResponse(ctx, invoke(ctx, req, AggregatedResult.EMPTY)); } diff --git a/core/src/main/java/com/linecorp/armeria/server/AbstractHttpResponseSubscriber.java b/core/src/main/java/com/linecorp/armeria/server/AbstractHttpResponseSubscriber.java index 9a2dd060269..97ab6156740 100644 --- a/core/src/main/java/com/linecorp/armeria/server/AbstractHttpResponseSubscriber.java +++ b/core/src/main/java/com/linecorp/armeria/server/AbstractHttpResponseSubscriber.java @@ -216,6 +216,7 @@ public void onNext(HttpObject o) { break; } case DONE: + assert subscription != null; isSubscriptionCompleted = true; subscription.cancel(); PooledObjects.close(o); diff --git a/core/src/main/java/com/linecorp/armeria/server/CorsServerErrorHandler.java b/core/src/main/java/com/linecorp/armeria/server/CorsServerErrorHandler.java index 94c3a8baad8..d8ea993a6f1 100644 --- a/core/src/main/java/com/linecorp/armeria/server/CorsServerErrorHandler.java +++ b/core/src/main/java/com/linecorp/armeria/server/CorsServerErrorHandler.java @@ -43,12 +43,13 @@ final class CorsServerErrorHandler implements ServerErrorHandler { this.serverErrorHandler = serverErrorHandler; } + @Nullable @Override - public @Nullable AggregatedHttpResponse renderStatus(@Nullable ServiceRequestContext ctx, - ServiceConfig serviceConfig, - @Nullable RequestHeaders headers, - HttpStatus status, @Nullable String description, - @Nullable Throwable cause) { + public AggregatedHttpResponse renderStatus(@Nullable ServiceRequestContext ctx, + ServiceConfig serviceConfig, + @Nullable RequestHeaders headers, + HttpStatus status, @Nullable String description, + @Nullable Throwable cause) { if (ctx == null) { return serverErrorHandler.renderStatus(null, serviceConfig, headers, status, description, cause); @@ -73,8 +74,9 @@ final class CorsServerErrorHandler implements ServerErrorHandler { return AggregatedHttpResponse.of(updatedResponseHeaders, res.content()); } + @Nullable @Override - public @Nullable HttpResponse onServiceException(ServiceRequestContext ctx, Throwable cause) { + public HttpResponse onServiceException(ServiceRequestContext ctx, Throwable cause) { if (cause instanceof HttpResponseException) { final HttpResponse oldRes = serverErrorHandler.onServiceException(ctx, cause); if (oldRes == null) { @@ -84,14 +86,21 @@ final class CorsServerErrorHandler implements ServerErrorHandler { if (corsService == null) { return oldRes; } - return oldRes - .recover(HttpResponseException.class, - ex -> ex.httpResponse() - .mapHeaders(oldHeaders -> addCorsHeaders(ctx, - corsService.config(), - oldHeaders))); + return oldRes.recover(HttpResponseException.class, ex -> { + return ex.httpResponse() + .mapHeaders(oldHeaders -> addCorsHeaders(ctx, corsService.config(), oldHeaders)); + }); } else { return serverErrorHandler.onServiceException(ctx, cause); } } + + @Nullable + @Override + public AggregatedHttpResponse onProtocolViolation(ServiceConfig config, + @Nullable RequestHeaders headers, + HttpStatus status, @Nullable String description, + @Nullable Throwable cause) { + return serverErrorHandler.onProtocolViolation(config, headers, status, description, cause); + } } diff --git a/core/src/main/java/com/linecorp/armeria/server/DefaultRoutingContext.java b/core/src/main/java/com/linecorp/armeria/server/DefaultRoutingContext.java index 8bdc50d2b0f..bdc51f2141b 100644 --- a/core/src/main/java/com/linecorp/armeria/server/DefaultRoutingContext.java +++ b/core/src/main/java/com/linecorp/armeria/server/DefaultRoutingContext.java @@ -165,6 +165,7 @@ public void deferStatusException(HttpStatusException deferredCause) { this.deferredCause = requireNonNull(deferredCause, "deferredCause"); } + @Nullable @Override public HttpStatusException deferredStatusException() { return deferredCause; diff --git a/core/src/main/java/com/linecorp/armeria/server/EmptyContentDecodedHttpRequest.java b/core/src/main/java/com/linecorp/armeria/server/EmptyContentDecodedHttpRequest.java index b71b57b64c5..f8485648293 100644 --- a/core/src/main/java/com/linecorp/armeria/server/EmptyContentDecodedHttpRequest.java +++ b/core/src/main/java/com/linecorp/armeria/server/EmptyContentDecodedHttpRequest.java @@ -89,6 +89,7 @@ public RoutingContext routingContext() { return routingContext; } + @Nullable @Override public Routed route() { if (routingContext.hasResult()) { diff --git a/core/src/main/java/com/linecorp/armeria/server/HttpServerCodec.java b/core/src/main/java/com/linecorp/armeria/server/HttpServerCodec.java index adbb29e49c6..f6dc34775c6 100644 --- a/core/src/main/java/com/linecorp/armeria/server/HttpServerCodec.java +++ b/core/src/main/java/com/linecorp/armeria/server/HttpServerCodec.java @@ -38,6 +38,8 @@ import java.util.List; import java.util.Queue; +import com.linecorp.armeria.common.annotation.Nullable; + import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.CombinedChannelDuplexHandler; @@ -64,7 +66,7 @@ final class HttpServerCodec extends CombinedChannelDuplexHandler queue = new ArrayDeque(); + private final Queue queue = new ArrayDeque<>(); /** * Creates a new instance with the default decoder options @@ -121,6 +123,7 @@ protected HttpMessage createMessage(String[] initialLine) throws Exception { private final class HttpServerResponseEncoder extends HttpResponseEncoder { + @Nullable private HttpMethod method; @Override diff --git a/core/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java b/core/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java index 30029c87c7c..f3722de3e91 100644 --- a/core/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java +++ b/core/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java @@ -812,9 +812,8 @@ private void handleRequestOrResponseComplete() { unfinishedRequests.remove(req); } - final boolean needsDisconnection = - ctx.channel().isActive() && - (handledLastRequest || responseEncoder.keepAliveHandler().needsDisconnection()); + final boolean needsDisconnection = ctx.channel().isActive() && + (handledLastRequest || isNeedsDisconnection()); if (needsDisconnection) { // Graceful shutdown mode: If a connection needs to be closed by `KeepAliveHandler` // such as a max connection age or `ServiceRequestContext.initiateConnectionShutdown()`, @@ -844,5 +843,10 @@ private void handleRequestOrResponseComplete() { logger.warn("Unexpected exception:", t); } } + + private boolean isNeedsDisconnection() { + assert responseEncoder != null; + return responseEncoder.keepAliveHandler().needsDisconnection(); + } } } diff --git a/core/src/main/java/com/linecorp/armeria/server/LengthBasedServiceNaming.java b/core/src/main/java/com/linecorp/armeria/server/LengthBasedServiceNaming.java index e9980388250..0cf7d00757f 100644 --- a/core/src/main/java/com/linecorp/armeria/server/LengthBasedServiceNaming.java +++ b/core/src/main/java/com/linecorp/armeria/server/LengthBasedServiceNaming.java @@ -20,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.internal.common.util.TargetLengthBasedClassNameAbbreviator; final class LengthBasedServiceNaming implements ServiceNaming { @@ -36,10 +37,11 @@ private LengthBasedServiceNaming(int shortenedServiceNameLength) { abbreviator = new TargetLengthBasedClassNameAbbreviator(shortenedServiceNameLength); } + @Nullable @Override public String serviceName(ServiceRequestContext ctx) { final String fullTypeName = ServiceNaming.fullTypeName().serviceName(ctx); - return abbreviate(fullTypeName); + return fullTypeName != null ? abbreviate(fullTypeName) : null; } private String abbreviate(String serviceName) { diff --git a/core/src/main/java/com/linecorp/armeria/server/RouteCache.java b/core/src/main/java/com/linecorp/armeria/server/RouteCache.java index 2004e484a30..f83ae0d71b8 100644 --- a/core/src/main/java/com/linecorp/armeria/server/RouteCache.java +++ b/core/src/main/java/com/linecorp/armeria/server/RouteCache.java @@ -66,9 +66,15 @@ final class RouteCache { */ static Router wrapVirtualHostRouter(Router delegate, Set dynamicPredicateRoutes) { - return FIND_CACHE == null ? delegate - : new CachingRouter<>(delegate, ServiceConfig::route, - FIND_CACHE, FIND_ALL_CACHE, dynamicPredicateRoutes); + if (FIND_CACHE == null) { + return delegate; + } + + assert FIND_ALL_CACHE != null; + return new CachingRouter<>(delegate, + ServiceConfig::route, + FIND_CACHE, FIND_ALL_CACHE, + dynamicPredicateRoutes); } /** @@ -77,11 +83,16 @@ static Router wrapVirtualHostRouter(Router delegat */ static Router wrapRouteDecoratingServiceRouter( Router delegate, Set dynamicPredicateRoutes) { - return DECORATOR_FIND_CACHE == null ? delegate - : new CachingRouter<>(delegate, RouteDecoratingService::route, - DECORATOR_FIND_CACHE, - DECORATOR_FIND_ALL_CACHE, - dynamicPredicateRoutes); + if (DECORATOR_FIND_CACHE == null) { + return delegate; + } + + assert DECORATOR_FIND_ALL_CACHE != null; + return new CachingRouter<>(delegate, + RouteDecoratingService::route, + DECORATOR_FIND_CACHE, + DECORATOR_FIND_ALL_CACHE, + dynamicPredicateRoutes); } private static Cache buildCache(String spec) { diff --git a/core/src/main/java/com/linecorp/armeria/server/Routed.java b/core/src/main/java/com/linecorp/armeria/server/Routed.java index e534b8eab4b..2b1312c23e8 100644 --- a/core/src/main/java/com/linecorp/armeria/server/Routed.java +++ b/core/src/main/java/com/linecorp/armeria/server/Routed.java @@ -83,6 +83,7 @@ public boolean isPresent() { */ public Route route() { ensurePresence(); + assert route != null; return route; } @@ -110,6 +111,7 @@ public RoutingResultType routingResultType() { */ public T value() { ensurePresence(); + assert value != null; return value; } diff --git a/core/src/main/java/com/linecorp/armeria/server/RoutingContextWrapper.java b/core/src/main/java/com/linecorp/armeria/server/RoutingContextWrapper.java index 86f117f3070..603cedb05fe 100644 --- a/core/src/main/java/com/linecorp/armeria/server/RoutingContextWrapper.java +++ b/core/src/main/java/com/linecorp/armeria/server/RoutingContextWrapper.java @@ -104,6 +104,7 @@ public void deferStatusException(HttpStatusException cause) { delegate.deferStatusException(cause); } + @Nullable @Override public HttpStatusException deferredStatusException() { return delegate.deferredStatusException(); diff --git a/core/src/main/java/com/linecorp/armeria/server/RoutingResult.java b/core/src/main/java/com/linecorp/armeria/server/RoutingResult.java index 0370ee3d66a..c8df71040a0 100644 --- a/core/src/main/java/com/linecorp/armeria/server/RoutingResult.java +++ b/core/src/main/java/com/linecorp/armeria/server/RoutingResult.java @@ -121,6 +121,7 @@ public boolean isPresent() { */ public String path() { ensurePresence(); + assert path != null; return path; } @@ -142,6 +143,7 @@ public String decodedPath() { return decodedPath; } + assert path != null; return this.decodedPath = ArmeriaHttpUtil.decodePath(path); } diff --git a/core/src/main/java/com/linecorp/armeria/server/Server.java b/core/src/main/java/com/linecorp/armeria/server/Server.java index ce7636bc6ac..9beb62f4ab2 100644 --- a/core/src/main/java/com/linecorp/armeria/server/Server.java +++ b/core/src/main/java/com/linecorp/armeria/server/Server.java @@ -550,6 +550,9 @@ private ChannelFuture doStart(ServerPort port) { return thread; }); + final GracefulShutdownSupport gracefulShutdownSupport = this.gracefulShutdownSupport; + assert gracefulShutdownSupport != null; + b.group(bossGroup, config.workerGroup()); b.handler(connectionLimitingHandler); b.childHandler(new HttpServerPipelineConfigurator(config, port, gracefulShutdownSupport, diff --git a/core/src/main/java/com/linecorp/armeria/server/Service.java b/core/src/main/java/com/linecorp/armeria/server/Service.java index 4a25975e5b7..b164cba3ed0 100644 --- a/core/src/main/java/com/linecorp/armeria/server/Service.java +++ b/core/src/main/java/com/linecorp/armeria/server/Service.java @@ -69,6 +69,7 @@ default void serviceAdded(ServiceConfig cfg) throws Exception {} * * @see Unwrappable */ + @Nullable @Override default T as(Class type) { requireNonNull(type, "type"); diff --git a/core/src/main/java/com/linecorp/armeria/server/ServiceConfig.java b/core/src/main/java/com/linecorp/armeria/server/ServiceConfig.java index 081fe770778..db18d1a6201 100644 --- a/core/src/main/java/com/linecorp/armeria/server/ServiceConfig.java +++ b/core/src/main/java/com/linecorp/armeria/server/ServiceConfig.java @@ -93,8 +93,8 @@ public final class ServiceConfig { /** * Creates a new instance. */ - ServiceConfig(Route route, Route mappedRoute, HttpService service, @Nullable String defaultLogName, - @Nullable String defaultServiceName, ServiceNaming defaultServiceNaming, + ServiceConfig(Route route, Route mappedRoute, HttpService service, @Nullable String defaultServiceName, + ServiceNaming defaultServiceNaming, @Nullable String defaultLogName, long requestTimeoutMillis, long maxRequestLength, boolean verboseResponses, AccessLogWriter accessLogWriter, BlockingTaskExecutor blockingTaskExecutor, diff --git a/core/src/main/java/com/linecorp/armeria/server/ServiceConfigBuilder.java b/core/src/main/java/com/linecorp/armeria/server/ServiceConfigBuilder.java index 993bb1c623f..07b8ce6b90f 100644 --- a/core/src/main/java/com/linecorp/armeria/server/ServiceConfigBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/server/ServiceConfigBuilder.java @@ -337,6 +337,7 @@ void defaultHeaders(HttpHeaders defaultHeaders) { } ServiceConfig build(ServiceNaming defaultServiceNaming, + @Nullable String defaultLogName, long defaultRequestTimeoutMillis, long defaultMaxRequestLength, boolean defaultVerboseResponses, @@ -374,8 +375,9 @@ ServiceConfig build(ServiceNaming defaultServiceNaming, return new ServiceConfig( routeWithBaseContextPath, mappedRoute == null ? routeWithBaseContextPath : mappedRoute, - service, defaultLogName, defaultServiceName, + service, defaultServiceName, this.defaultServiceNaming != null ? this.defaultServiceNaming : defaultServiceNaming, + this.defaultLogName != null ? this.defaultLogName : defaultLogName, requestTimeoutMillis, maxRequestLength, verboseResponses != null ? verboseResponses : defaultVerboseResponses, @@ -398,6 +400,7 @@ public String toString() { .add("route", route) .add("service", service) .add("defaultServiceNaming", defaultServiceNaming) + .add("defaultLogName", defaultLogName) .add("requestTimeoutMillis", requestTimeoutMillis) .add("maxRequestLength", maxRequestLength) .add("verboseResponses", verboseResponses) diff --git a/core/src/main/java/com/linecorp/armeria/server/VirtualHost.java b/core/src/main/java/com/linecorp/armeria/server/VirtualHost.java index 72559bd1520..89ddb678a29 100644 --- a/core/src/main/java/com/linecorp/armeria/server/VirtualHost.java +++ b/core/src/main/java/com/linecorp/armeria/server/VirtualHost.java @@ -92,6 +92,7 @@ public final class VirtualHost { private final Logger accessLogger; private final ServiceNaming defaultServiceNaming; + @Nullable private final String defaultLogName; private final long requestTimeoutMillis; private final long maxRequestLength; @@ -114,7 +115,7 @@ public final class VirtualHost { RejectedRouteHandler rejectionHandler, Function accessLoggerMapper, ServiceNaming defaultServiceNaming, - String defaultLogName, + @Nullable String defaultLogName, long requestTimeoutMillis, long maxRequestLength, boolean verboseResponses, AccessLogWriter accessLogWriter, @@ -362,6 +363,7 @@ public ServiceNaming defaultServiceNaming() { * Returns the default value of the {@link RequestLog#name()} property which is used when no name was set * via {@link RequestLogBuilder#name(String, String)}. */ + @Nullable public String defaultLogName() { return defaultLogName; } diff --git a/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java b/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java index ea946e27a19..323f7e04300 100644 --- a/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java @@ -1325,24 +1325,36 @@ VirtualHost build(VirtualHostBuilder template, DependencyInjector dependencyInje ensureHostnamePatternMatchesDefaultHostname(hostnamePattern, defaultHostname); // Retrieve all settings as a local copy. Use default builder's properties if not set. + assert template.defaultServiceNaming != null; final ServiceNaming defaultServiceNaming = this.defaultServiceNaming != null ? this.defaultServiceNaming : template.defaultServiceNaming; + final String defaultLogName = this.defaultLogName != null ? this.defaultLogName : template.defaultLogName; + + assert template.requestTimeoutMillis != null; final long requestTimeoutMillis = this.requestTimeoutMillis != null ? this.requestTimeoutMillis : template.requestTimeoutMillis; + + assert template.maxRequestLength != null; final long maxRequestLength = this.maxRequestLength != null ? this.maxRequestLength : template.maxRequestLength; + + assert template.verboseResponses != null; final boolean verboseResponses = this.verboseResponses != null ? this.verboseResponses : template.verboseResponses; + + assert template.requestAutoAbortDelayMillis != null; final long requestAutoAbortDelayMillis = this.requestAutoAbortDelayMillis != null ? this.requestAutoAbortDelayMillis : template.requestAutoAbortDelayMillis; + + assert template.rejectedRouteHandler != null; final RejectedRouteHandler rejectedRouteHandler = this.rejectedRouteHandler != null ? this.rejectedRouteHandler : template.rejectedRouteHandler; @@ -1428,9 +1440,9 @@ VirtualHost build(VirtualHostBuilder template, DependencyInjector dependencyInje cfgSetters.getClass().getSimpleName()); } }).map(cfgBuilder -> { - return cfgBuilder.build(defaultServiceNaming, requestTimeoutMillis, maxRequestLength, - verboseResponses, accessLogWriter, blockingTaskExecutor, - successFunction, requestAutoAbortDelayMillis, + return cfgBuilder.build(defaultServiceNaming, defaultLogName, requestTimeoutMillis, + maxRequestLength, verboseResponses, accessLogWriter, + blockingTaskExecutor, successFunction, requestAutoAbortDelayMillis, multipartUploadsLocation, multipartRemovalStrategy, serviceWorkerGroup, defaultHeaders, requestIdGenerator, defaultErrorHandler, @@ -1439,8 +1451,8 @@ VirtualHost build(VirtualHostBuilder template, DependencyInjector dependencyInje final ServiceConfig fallbackServiceConfig = new ServiceConfigBuilder(RouteBuilder.FALLBACK_ROUTE, "/", FallbackService.INSTANCE) - .build(defaultServiceNaming, requestTimeoutMillis, maxRequestLength, verboseResponses, - accessLogWriter, blockingTaskExecutor, successFunction, + .build(defaultServiceNaming, defaultLogName, requestTimeoutMillis, maxRequestLength, + verboseResponses, accessLogWriter, blockingTaskExecutor, successFunction, requestAutoAbortDelayMillis, multipartUploadsLocation, multipartRemovalStrategy, serviceWorkerGroup, defaultHeaders, requestIdGenerator, defaultErrorHandler, unloggedExceptionsReporter, "/", contextHook); @@ -1493,6 +1505,7 @@ private SslContext sslContext(VirtualHostBuilder template, TlsEngineType tlsEngi SslContext sslContext = null; boolean releaseSslContextOnFailure = false; try { + assert template.tlsAllowUnsafeCiphers != null; final boolean tlsAllowUnsafeCiphers = this.tlsAllowUnsafeCiphers != null ? this.tlsAllowUnsafeCiphers : template.tlsAllowUnsafeCiphers; @@ -1521,6 +1534,7 @@ private SslContext sslContext(VirtualHostBuilder template, TlsEngineType tlsEngi tlsCustomizers = this.tlsCustomizers; sslContextFromThis = true; } else { + assert template.tlsSelfSigned != null; tlsSelfSigned = template.tlsSelfSigned; tlsCustomizers = template.tlsCustomizers; } @@ -1562,6 +1576,7 @@ private SslContext sslContext(VirtualHostBuilder template, TlsEngineType tlsEngi private SelfSignedCertificate selfSignedCertificate() throws CertificateException { if (selfSignedCertificate == null) { + assert defaultHostname != null; return selfSignedCertificate = new SelfSignedCertificate(defaultHostname); } return selfSignedCertificate; diff --git a/core/src/main/java/com/linecorp/armeria/server/annotation/ByteArrayResponseConverterFunction.java b/core/src/main/java/com/linecorp/armeria/server/annotation/ByteArrayResponseConverterFunction.java index 4f35ecd7dce..cd85376d551 100644 --- a/core/src/main/java/com/linecorp/armeria/server/annotation/ByteArrayResponseConverterFunction.java +++ b/core/src/main/java/com/linecorp/armeria/server/annotation/ByteArrayResponseConverterFunction.java @@ -45,6 +45,7 @@ */ public final class ByteArrayResponseConverterFunction implements ResponseConverterFunction { + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType produceType) { final Class clazz = typeToClass(unwrapUnaryAsyncType(returnType)); diff --git a/core/src/main/java/com/linecorp/armeria/server/annotation/DefaultHttpResult.java b/core/src/main/java/com/linecorp/armeria/server/annotation/DefaultHttpResult.java index 23dc6cc4163..784e6f127fb 100644 --- a/core/src/main/java/com/linecorp/armeria/server/annotation/DefaultHttpResult.java +++ b/core/src/main/java/com/linecorp/armeria/server/annotation/DefaultHttpResult.java @@ -49,6 +49,7 @@ public HttpHeaders headers() { return headers; } + @Nullable @Override public T content() { return content; diff --git a/core/src/main/java/com/linecorp/armeria/server/annotation/HttpFileResponseConverterFunction.java b/core/src/main/java/com/linecorp/armeria/server/annotation/HttpFileResponseConverterFunction.java index 99e609f115a..ea48303b317 100644 --- a/core/src/main/java/com/linecorp/armeria/server/annotation/HttpFileResponseConverterFunction.java +++ b/core/src/main/java/com/linecorp/armeria/server/annotation/HttpFileResponseConverterFunction.java @@ -39,6 +39,7 @@ */ public final class HttpFileResponseConverterFunction implements ResponseConverterFunction { + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType produceType) { final Class clazz = typeToClass(unwrapUnaryAsyncType(returnType)); diff --git a/core/src/main/java/com/linecorp/armeria/server/annotation/JacksonResponseConverterFunction.java b/core/src/main/java/com/linecorp/armeria/server/annotation/JacksonResponseConverterFunction.java index 773040ac340..5afee50a01c 100644 --- a/core/src/main/java/com/linecorp/armeria/server/annotation/JacksonResponseConverterFunction.java +++ b/core/src/main/java/com/linecorp/armeria/server/annotation/JacksonResponseConverterFunction.java @@ -71,6 +71,7 @@ public JacksonResponseConverterFunction(ObjectMapper mapper) { this.mapper = requireNonNull(mapper, "mapper"); } + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType produceType) { final Class clazz = typeToClass(unwrapUnaryAsyncType(returnType)); diff --git a/core/src/main/java/com/linecorp/armeria/server/annotation/NullToNoContentResponseConverterFunction.java b/core/src/main/java/com/linecorp/armeria/server/annotation/NullToNoContentResponseConverterFunction.java index 6819a9e2495..39a367e6f99 100644 --- a/core/src/main/java/com/linecorp/armeria/server/annotation/NullToNoContentResponseConverterFunction.java +++ b/core/src/main/java/com/linecorp/armeria/server/annotation/NullToNoContentResponseConverterFunction.java @@ -31,6 +31,7 @@ */ public final class NullToNoContentResponseConverterFunction implements ResponseConverterFunction { + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType contentType) { return null; diff --git a/core/src/main/java/com/linecorp/armeria/server/annotation/ServerSentEventResponseConverterFunction.java b/core/src/main/java/com/linecorp/armeria/server/annotation/ServerSentEventResponseConverterFunction.java index 18eb198c12a..4f0026f7e4e 100644 --- a/core/src/main/java/com/linecorp/armeria/server/annotation/ServerSentEventResponseConverterFunction.java +++ b/core/src/main/java/com/linecorp/armeria/server/annotation/ServerSentEventResponseConverterFunction.java @@ -42,6 +42,7 @@ */ public final class ServerSentEventResponseConverterFunction implements ResponseConverterFunction { + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType contentType) { final Class clazz = typeToClass(unwrapUnaryAsyncType(returnType)); diff --git a/core/src/main/java/com/linecorp/armeria/server/annotation/StringResponseConverterFunction.java b/core/src/main/java/com/linecorp/armeria/server/annotation/StringResponseConverterFunction.java index 28f415b8bb1..55a49e9a74c 100644 --- a/core/src/main/java/com/linecorp/armeria/server/annotation/StringResponseConverterFunction.java +++ b/core/src/main/java/com/linecorp/armeria/server/annotation/StringResponseConverterFunction.java @@ -45,6 +45,7 @@ */ public final class StringResponseConverterFunction implements ResponseConverterFunction { + @Nullable @Override public Boolean isResponseStreaming(Type resultType, @Nullable MediaType contentType) { if (contentType != null && contentType.is(MediaType.ANY_TEXT_TYPE)) { diff --git a/core/src/main/java/com/linecorp/armeria/server/docs/DocService.java b/core/src/main/java/com/linecorp/armeria/server/docs/DocService.java index e1e30217899..0e30c1e0f00 100644 --- a/core/src/main/java/com/linecorp/armeria/server/docs/DocService.java +++ b/core/src/main/java/com/linecorp/armeria/server/docs/DocService.java @@ -57,6 +57,7 @@ import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.common.MediaType; +import com.linecorp.armeria.common.ResponseHeaders; import com.linecorp.armeria.common.ServerCacheControl; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.ThreadFactories; @@ -75,6 +76,7 @@ import com.linecorp.armeria.server.file.AggregatedHttpFile; import com.linecorp.armeria.server.file.FileService; import com.linecorp.armeria.server.file.HttpFile; +import com.linecorp.armeria.server.file.HttpFileAttributes; import com.linecorp.armeria.server.file.HttpFileBuilder; import com.linecorp.armeria.server.file.HttpVfs; import com.linecorp.armeria.server.file.MediaTypeResolver; @@ -422,11 +424,18 @@ public HttpFile get( if (specificationLoader.contains(path)) { return HttpFile.from(specificationLoader.get(path).thenApply(file -> { assert file != AggregatedHttpFile.nonExistent(); - final HttpFileBuilder builder = HttpFile.builder(file.content(), - file.attributes().lastModifiedMillis()); + final HttpData fileContent = file.content(); + final ResponseHeaders fileHeaders = file.headers(); + final HttpFileAttributes fileAttrs = file.attributes(); + assert fileContent != null; + assert fileHeaders != null; + assert fileAttrs != null; + + final HttpFileBuilder builder = HttpFile.builder(fileContent, + fileAttrs.lastModifiedMillis()); builder.autoDetectedContentType(false); builder.clock(clock); - builder.setHeaders(file.headers()); + builder.setHeaders(fileHeaders); builder.setHeaders(additionalHeaders); if (contentEncoding != null) { builder.setHeader(HttpHeaderNames.CONTENT_ENCODING, contentEncoding); diff --git a/core/src/main/java/com/linecorp/armeria/server/docs/MethodInfo.java b/core/src/main/java/com/linecorp/armeria/server/docs/MethodInfo.java index b1a5cb5628f..fc1813c9e51 100644 --- a/core/src/main/java/com/linecorp/armeria/server/docs/MethodInfo.java +++ b/core/src/main/java/com/linecorp/armeria/server/docs/MethodInfo.java @@ -159,7 +159,9 @@ public MethodInfo(String serviceName, String name, for (String query : exampleQueries) { final RequestTarget reqTarget = RequestTarget.forServer("/?" + query); checkArgument(reqTarget != null, "exampleQueries contains an invalid query string: %s", query); - exampleQueriesBuilder.add(reqTarget.query()); + final String safeQuery = reqTarget.query(); + assert safeQuery != null; + exampleQueriesBuilder.add(safeQuery); } this.exampleQueries = exampleQueriesBuilder.build(); diff --git a/core/src/main/java/com/linecorp/armeria/server/file/AbstractHttpFile.java b/core/src/main/java/com/linecorp/armeria/server/file/AbstractHttpFile.java index c9df77332c0..620c9cdbc65 100644 --- a/core/src/main/java/com/linecorp/armeria/server/file/AbstractHttpFile.java +++ b/core/src/main/java/com/linecorp/armeria/server/file/AbstractHttpFile.java @@ -209,6 +209,7 @@ private HttpResponse read(Executor fileReadExecutor, ByteBufAllocator alloc, return null; } + assert attrs != null; final long length = attrs.length(); if (length == 0) { // No need to stream an empty file. diff --git a/core/src/main/java/com/linecorp/armeria/server/file/FileService.java b/core/src/main/java/com/linecorp/armeria/server/file/FileService.java index 09cf83e3ead..7f460cc6e82 100644 --- a/core/src/main/java/com/linecorp/armeria/server/file/FileService.java +++ b/core/src/main/java/com/linecorp/armeria/server/file/FileService.java @@ -23,6 +23,7 @@ import java.nio.file.Path; import java.util.EnumSet; import java.util.Iterator; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -257,35 +258,18 @@ private HttpFile findFile(ServiceRequestContext ctx, HttpRequest req) { }); }); } else { - // Redirect to the slash appended path if: - // 1) /index.html exists or - // 2) it has a directory listing. - final String indexPath = decodedMappedPath + "/index.html"; - return findFile(ctx, indexPath, encodings, decompress).thenCompose(indexFile -> { - if (indexFile != null) { - return UnmodifiableFuture.completedFuture(true); - } + final List fallbackExtensions = config.fallbackFileExtensions(); + if (fallbackExtensions.isEmpty()) { + return findFileWithIndexPath(ctx, decodedMappedPath, encodings, decompress); + } - if (!config.autoIndex()) { - return UnmodifiableFuture.completedFuture(false); - } - - return config.vfs().canList(ctx.blockingTaskExecutor(), decodedMappedPath); - }).thenApply(canList -> { - if (canList) { - try (TemporaryThreadLocals ttl = TemporaryThreadLocals.acquire()) { - final StringBuilder locationBuilder = ttl.stringBuilder() - .append(ctx.path()) - .append('/'); - if (ctx.query() != null) { - locationBuilder.append('?') - .append(ctx.query()); - } - return HttpFile.ofRedirect(locationBuilder.toString()); - } - } else { - return HttpFile.nonExistent(); + // Try appending file extensions if it was a file access and file extensions are configured. + return findFileWithExtensions(ctx, fallbackExtensions.iterator(), decodedMappedPath, + encodings, decompress).thenCompose(fileWithExtension -> { + if (fileWithExtension != null) { + return UnmodifiableFuture.completedFuture(fileWithExtension); } + return findFileWithIndexPath(ctx, decodedMappedPath, encodings, decompress); }); } })); @@ -385,6 +369,58 @@ private HttpFile findFile(ServiceRequestContext ctx, HttpRequest req) { }); } + private CompletableFuture<@Nullable HttpFile> findFileWithIndexPath( + ServiceRequestContext ctx, String decodedMappedPath, + Set encodings, boolean decompress) { + // Redirect to the slash appended path if: + // 1) /index.html exists or + // 2) it has a directory listing. + final String indexPath = decodedMappedPath + "/index.html"; + return findFile(ctx, indexPath, encodings, decompress).thenCompose(indexFile -> { + if (indexFile != null) { + return UnmodifiableFuture.completedFuture(true); + } + + if (!config.autoIndex()) { + return UnmodifiableFuture.completedFuture(false); + } + + return config.vfs().canList(ctx.blockingTaskExecutor(), decodedMappedPath); + }).thenApply(canList -> { + if (canList) { + try (TemporaryThreadLocals ttl = TemporaryThreadLocals.acquire()) { + final StringBuilder locationBuilder = ttl.stringBuilder() + .append(ctx.path()) + .append('/'); + if (ctx.query() != null) { + locationBuilder.append('?') + .append(ctx.query()); + } + return HttpFile.ofRedirect(locationBuilder.toString()); + } + } else { + return HttpFile.nonExistent(); + } + }); + } + + private CompletableFuture<@Nullable HttpFile> findFileWithExtensions( + ServiceRequestContext ctx, @Nullable Iterator extensionIterator, String path, + Set supportedEncodings, boolean decompress) { + if (extensionIterator == null || !extensionIterator.hasNext()) { + return UnmodifiableFuture.completedFuture(null); + } + + final String extension = extensionIterator.next(); + return findFile(ctx, path + '.' + extension, supportedEncodings, decompress).thenCompose(file -> { + if (file != null) { + return UnmodifiableFuture.completedFuture(file); + } + + return findFileWithExtensions(ctx, extensionIterator, path, supportedEncodings, decompress); + }); + } + private CompletableFuture<@Nullable HttpFile> findFileAndDecompress( ServiceRequestContext ctx, String path, Set supportedEncodings) { // Look up a non-compressed file first to avoid extra decompression @@ -409,7 +445,9 @@ private HttpFile cache(ServiceRequestContext ctx, PathAndEncoding pathAndEncodin if (decompress && encoding != null) { assert aggregated instanceof HttpDataFile; aggregated = decompress((HttpDataFile) aggregated, encoding, alloc); - if (aggregated.attributes().length() > config.maxCacheEntrySizeBytes()) { + final HttpFileAttributes attrs = aggregated.attributes(); + assert attrs != null; + if (attrs.length() > config.maxCacheEntrySizeBytes()) { // Invalidate the cache just in case the file was small previously. cache.invalidate(pathAndEncoding); return aggregated.toHttpFile(); diff --git a/core/src/main/java/com/linecorp/armeria/server/file/FileServiceBuilder.java b/core/src/main/java/com/linecorp/armeria/server/file/FileServiceBuilder.java index 9b303191edf..1366e6ab26d 100644 --- a/core/src/main/java/com/linecorp/armeria/server/file/FileServiceBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/server/file/FileServiceBuilder.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.server.file; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.linecorp.armeria.server.file.FileServiceConfig.validateEntryCacheSpec; import static com.linecorp.armeria.server.file.FileServiceConfig.validateMaxCacheEntrySizeBytes; @@ -23,9 +24,11 @@ import static java.util.Objects.requireNonNull; import java.time.Clock; +import java.util.List; import java.util.Map.Entry; import com.github.benmanes.caffeine.cache.CaffeineSpec; +import com.google.common.collect.ImmutableList; import com.linecorp.armeria.common.CacheControl; import com.linecorp.armeria.common.Flags; @@ -35,6 +38,7 @@ import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.annotation.Nullable; +import com.linecorp.armeria.common.annotation.UnstableApi; /** * Builds a new {@link FileService} and its {@link FileServiceConfig}. Use the factory methods in @@ -60,6 +64,9 @@ public final class FileServiceBuilder { HttpHeadersBuilder headers; MediaTypeResolver mediaTypeResolver = MediaTypeResolver.ofDefault(); + @Nullable + private ImmutableList.Builder fallbackFileExtensions; + FileServiceBuilder(HttpVfs vfs) { this.vfs = requireNonNull(vfs, "vfs"); } @@ -153,6 +160,46 @@ public FileServiceBuilder autoIndex(boolean autoIndex) { return this; } + /** + * Adds the file extensions to be considered when resolving file names. + * This method allows specifying alternative file names by appending the provided extensions + * to the requested file name if the initially requested resource is not found. + * + *

For instance, if {@code "/index"} is requested and {@code "html"} is an added extension, + * {@link FileService} will attempt to serve {@code "/index.html"} if {@code "/index"} is not found. + */ + @UnstableApi + public FileServiceBuilder fallbackFileExtensions(String... extensions) { + requireNonNull(extensions, "extensions"); + return fallbackFileExtensions(ImmutableList.copyOf(extensions)); + } + + /** + * Adds the file extensions to be considered when resolving file names. + * This method allows specifying alternative file names by appending the provided extensions + * to the requested file name if the initially requested resource is not found. + * + *

For instance, if {@code "/index"} is requested and {@code "html"} is an added extension, + * {@link FileService} will attempt to serve {@code "/index.html"} if {@code "/index"} is not found. + */ + @UnstableApi + public FileServiceBuilder fallbackFileExtensions(Iterable extensions) { + requireNonNull(extensions, "extensions"); + for (String extension : extensions) { + checkArgument(!extension.isEmpty(), "extension is empty"); + checkArgument(extension.charAt(0) != '.', "extension: %s (expected: without a dot)", extension); + } + if (fallbackFileExtensions == null) { + fallbackFileExtensions = ImmutableList.builder(); + } + fallbackFileExtensions.addAll(extensions); + return this; + } + + private List fallbackFileExtensions() { + return fallbackFileExtensions != null ? fallbackFileExtensions.build() : ImmutableList.of(); + } + /** * Returns the immutable additional {@link HttpHeaders} which will be set when building an * {@link HttpResponse}. @@ -248,12 +295,13 @@ public FileService build() { return new FileService(new FileServiceConfig( vfs, clock, entryCacheSpec, maxCacheEntrySizeBytes, serveCompressedFiles, autoDecompress, autoIndex, buildHeaders(), - mediaTypeResolver.orElse(MediaTypeResolver.ofDefault()))); + mediaTypeResolver.orElse(MediaTypeResolver.ofDefault()), fallbackFileExtensions())); } @Override public String toString() { return FileServiceConfig.toString(this, vfs, clock, entryCacheSpec, maxCacheEntrySizeBytes, - serveCompressedFiles, autoIndex, headers, mediaTypeResolver); + serveCompressedFiles, autoIndex, headers, mediaTypeResolver, + fallbackFileExtensions()); } } diff --git a/core/src/main/java/com/linecorp/armeria/server/file/FileServiceConfig.java b/core/src/main/java/com/linecorp/armeria/server/file/FileServiceConfig.java index 401438092e4..8766acd95a7 100644 --- a/core/src/main/java/com/linecorp/armeria/server/file/FileServiceConfig.java +++ b/core/src/main/java/com/linecorp/armeria/server/file/FileServiceConfig.java @@ -19,6 +19,7 @@ import static java.util.Objects.requireNonNull; import java.time.Clock; +import java.util.List; import java.util.Map.Entry; import com.github.benmanes.caffeine.cache.CaffeineSpec; @@ -28,6 +29,7 @@ import com.linecorp.armeria.common.HttpHeaders; import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.annotation.Nullable; +import com.linecorp.armeria.common.annotation.UnstableApi; import io.netty.util.AsciiString; @@ -46,10 +48,12 @@ public final class FileServiceConfig { private final boolean autoIndex; private final HttpHeaders headers; private final MediaTypeResolver mediaTypeResolver; + private final List fallbackFileExtensions; FileServiceConfig(HttpVfs vfs, Clock clock, @Nullable String entryCacheSpec, int maxCacheEntrySizeBytes, boolean serveCompressedFiles, boolean autoDecompress, boolean autoIndex, - HttpHeaders headers, MediaTypeResolver mediaTypeResolver) { + HttpHeaders headers, MediaTypeResolver mediaTypeResolver, + List fallbackFileExtensions) { this.vfs = requireNonNull(vfs, "vfs"); this.clock = requireNonNull(clock, "clock"); this.entryCacheSpec = validateEntryCacheSpec(entryCacheSpec); @@ -59,6 +63,7 @@ public final class FileServiceConfig { this.autoIndex = autoIndex; this.headers = requireNonNull(headers, "headers"); this.mediaTypeResolver = requireNonNull(mediaTypeResolver, "mediaTypeResolver"); + this.fallbackFileExtensions = requireNonNull(fallbackFileExtensions, "fallbackFileExtensions"); } @Nullable @@ -152,17 +157,26 @@ public MediaTypeResolver mediaTypeResolver() { return mediaTypeResolver; } + /** + * Returns the file extensions that are appended to the file name when the file is not found. + */ + @UnstableApi + public List fallbackFileExtensions() { + return fallbackFileExtensions; + } + @Override public String toString() { return toString(this, vfs(), clock(), entryCacheSpec(), maxCacheEntrySizeBytes(), - serveCompressedFiles(), autoIndex(), headers(), mediaTypeResolver()); + serveCompressedFiles(), autoIndex(), headers(), mediaTypeResolver(), + fallbackFileExtensions()); } static String toString(Object holder, HttpVfs vfs, Clock clock, @Nullable String entryCacheSpec, int maxCacheEntrySizeBytes, boolean serveCompressedFiles, boolean autoIndex, @Nullable Iterable> headers, - MediaTypeResolver mediaTypeResolver) { + MediaTypeResolver mediaTypeResolver, @Nullable List fallbackFileExtensions) { return MoreObjects.toStringHelper(holder).omitNullValues() .add("vfs", vfs) @@ -173,6 +187,7 @@ static String toString(Object holder, HttpVfs vfs, Clock clock, .add("autoIndex", autoIndex) .add("headers", headers) .add("mediaTypeResolver", mediaTypeResolver) + .add("fallbackFileExtensions", fallbackFileExtensions) .toString(); } } diff --git a/core/src/main/java/com/linecorp/armeria/server/file/FileSystemHttpFile.java b/core/src/main/java/com/linecorp/armeria/server/file/FileSystemHttpFile.java index 535d18961a4..67dbac22100 100644 --- a/core/src/main/java/com/linecorp/armeria/server/file/FileSystemHttpFile.java +++ b/core/src/main/java/com/linecorp/armeria/server/file/FileSystemHttpFile.java @@ -82,6 +82,7 @@ public CompletableFuture readAttributes(Executor fileReadExe }, fileReadExecutor); } + @Nullable @Override protected ByteChannel newStream() throws IOException { try { diff --git a/core/src/main/java/com/linecorp/armeria/server/file/HttpDataFile.java b/core/src/main/java/com/linecorp/armeria/server/file/HttpDataFile.java index 042dc6aae50..bc1660b17c1 100644 --- a/core/src/main/java/com/linecorp/armeria/server/file/HttpDataFile.java +++ b/core/src/main/java/com/linecorp/armeria/server/file/HttpDataFile.java @@ -90,7 +90,9 @@ public CompletableFuture readAttributes(Executor fileReadExe @Nonnull @Override public ResponseHeaders headers() { - return readHeaders(attrs); + final ResponseHeaders headers = readHeaders(attrs); + assert headers != null; + return headers; } @Override diff --git a/core/src/main/java/com/linecorp/armeria/server/file/StreamingHttpFile.java b/core/src/main/java/com/linecorp/armeria/server/file/StreamingHttpFile.java index c88472bda6c..a2ac81ce69d 100644 --- a/core/src/main/java/com/linecorp/armeria/server/file/StreamingHttpFile.java +++ b/core/src/main/java/com/linecorp/armeria/server/file/StreamingHttpFile.java @@ -79,6 +79,7 @@ protected StreamingHttpFile(@Nullable MediaType contentType, super(contentType, clock, dateEnabled, lastModifiedEnabled, entityTagFunction, headers); } + @Nullable @Override protected final HttpResponse doRead(ResponseHeaders headers, long length, Executor fileReadExecutor, ByteBufAllocator alloc) throws IOException { diff --git a/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogComponent.java b/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogComponent.java index aac8428eed4..065063dea74 100644 --- a/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogComponent.java +++ b/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogComponent.java @@ -376,6 +376,7 @@ AsciiString headerName() { return headerName; } + @Nullable @Override public Object getMessage0(RequestLog log) { return httpHeaders.apply(log).get(headerName); diff --git a/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogFormats.java b/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogFormats.java index 24cfd034e63..f4ece1c8fe7 100644 --- a/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogFormats.java +++ b/core/src/main/java/com/linecorp/armeria/server/logging/AccessLogFormats.java @@ -133,6 +133,7 @@ static List parseCustom(String formatStr) { textBuilder.append(ch); } else { if (textBuilder.length() > 0) { + assert condBuilder != null; condBuilder.addHttpStatus(newStringAndReset(textBuilder)); } // Loop again. diff --git a/core/src/main/resources/com/linecorp/armeria/public_suffixes.txt b/core/src/main/resources/com/linecorp/armeria/public_suffixes.txt index 705fc8081c0..1e4d80ac161 100644 --- a/core/src/main/resources/com/linecorp/armeria/public_suffixes.txt +++ b/core/src/main/resources/com/linecorp/armeria/public_suffixes.txt @@ -11,13 +11,16 @@ *.advisor.ws *.af-south-1.airflow.amazonaws.com *.alces.network -*.amplifyapp.com *.ap-east-1.airflow.amazonaws.com *.ap-northeast-1.airflow.amazonaws.com *.ap-northeast-2.airflow.amazonaws.com +*.ap-northeast-3.airflow.amazonaws.com *.ap-south-1.airflow.amazonaws.com +*.ap-south-2.airflow.amazonaws.com *.ap-southeast-1.airflow.amazonaws.com *.ap-southeast-2.airflow.amazonaws.com +*.ap-southeast-3.airflow.amazonaws.com +*.ap-southeast-4.airflow.amazonaws.com *.awdev.ca *.awsapprunner.com *.azurecontainer.io @@ -30,6 +33,7 @@ *.bzz.dapps.earth *.c.ts.net *.ca-central-1.airflow.amazonaws.com +*.ca-west-1.airflow.amazonaws.com *.ck *.cloud.metacentrum.cz *.cloudera.site @@ -57,14 +61,17 @@ *.elb.amazonaws.com.cn *.er *.eu-central-1.airflow.amazonaws.com +*.eu-central-2.airflow.amazonaws.com *.eu-north-1.airflow.amazonaws.com *.eu-south-1.airflow.amazonaws.com +*.eu-south-2.airflow.amazonaws.com *.eu-west-1.airflow.amazonaws.com *.eu-west-2.airflow.amazonaws.com *.eu-west-3.airflow.amazonaws.com *.ewp.live *.ex.futurecms.at *.ex.ortsinfo.at +*.experiments.sagemaker.aws *.firenet.ch *.fk *.frusky.de @@ -74,6 +81,7 @@ *.hosting.myjino.ru *.hosting.ovh.net *.id.pub +*.il-central-1.airflow.amazonaws.com *.in.futurecms.at *.jm *.kawasaki.jp @@ -88,6 +96,7 @@ *.lclstage.dev *.linodeobjects.com *.magentosite.cloud +*.me-central-1.airflow.amazonaws.com *.me-south-1.airflow.amazonaws.com *.migration.run *.mm @@ -485,9 +494,9 @@ ami.ibaraki.jp amica amli.no amot.no +amplifyapp.com amscompute.com amsterdam -amusement.aero an.it analytics analytics-gateway.ap-northeast-1.amazonaws.com @@ -704,6 +713,7 @@ auth-fips.us-gov-west-1.amazoncognito.com auth-fips.us-west-1.amazoncognito.com auth-fips.us-west-2.amazoncognito.com auth.af-south-1.amazoncognito.com +auth.ap-east-1.amazoncognito.com auth.ap-northeast-1.amazoncognito.com auth.ap-northeast-2.amazoncognito.com auth.ap-northeast-3.amazoncognito.com @@ -714,6 +724,7 @@ auth.ap-southeast-2.amazoncognito.com auth.ap-southeast-3.amazoncognito.com auth.ap-southeast-4.amazoncognito.com auth.ca-central-1.amazoncognito.com +auth.ca-west-1.amazoncognito.com auth.eu-central-1.amazoncognito.com auth.eu-central-2.amazoncognito.com auth.eu-north-1.amazoncognito.com @@ -751,7 +762,6 @@ awaji.hyogo.jp aws awsapps.com awsglobalaccelerator.com -awsmppl.com ax axa aya.miyazaki.jp @@ -786,7 +796,6 @@ babymilk.jp bacgiang.vn backan.vn backdrop.jp -backplaneapp.io baclieu.vn bacninh.vn badaddja.no @@ -1204,7 +1213,6 @@ bz.it bzh c.bg c.cdn77.org -c.la c.se c66.me ca @@ -1492,8 +1500,6 @@ cloudaccess.host cloudaccess.net cloudapp.net cloudapps.digital -cloudcontrolapp.com -cloudcontrolled.com cloudflare-ipfs.com cloudflare.net cloudfront.net @@ -1858,7 +1864,6 @@ cu cuiaba.br cuisinella cuneo.it -cupcake.is curitiba.br curv.dev cust.cloudscale.ch @@ -1878,11 +1883,6 @@ cx cx.ua cy cy.eu.org -cya.gg -cyclic-app.com -cyclic.app -cyclic.cloud -cyclic.co.in cymru cyon.link cyon.site @@ -1997,6 +1997,7 @@ development.run devices.resinstaging.io df.gov.br df.leg.br +dfirma.pl dgca.aero dh.bytemark.co.uk dhl @@ -2030,6 +2031,7 @@ diy dj dk dk.eu.org +dkonto.pl dlugoleka.pl dm dn.ua @@ -2659,12 +2661,6 @@ fi.it fidelity fido fie.ee -filegear-au.me -filegear-de.me -filegear-gb.me -filegear-ie.me -filegear-jp.me -filegear-sg.me filegear.me film film.hu @@ -2728,7 +2724,6 @@ flt.cloud.muni.cz flutterflow.app fly fly.dev -flynnhosting.net fm fm.br fm.it @@ -2800,6 +2795,7 @@ freemyip.com freesite.host freetls.fastly.net frei.no +freight.aero frenchkiss.jp fresenius friuli-v-giulia.it @@ -3288,7 +3284,6 @@ grane.no granvin.no graphic.design graphics -graphox.us gratangen.no gratis grayjayleagues.com @@ -3779,8 +3774,6 @@ imizu.toyama.jp immo immobilien imperia.it -impertrix.com -impertrixcdn.com in in-addr.arpa in-berlin.de @@ -5018,6 +5011,7 @@ maringa.br marker.no market marketing +marketplace.aero markets marnardal.no marriott @@ -5271,7 +5265,6 @@ minoh.osaka.jp minokamo.gifu.jp minowa.nagano.jp mint -mintere.site mints.ne.jp mircloud.host mircloud.ru @@ -5412,7 +5405,6 @@ motoyama.kochi.jp mov movie movimiento.bo -mozilla-iot.org mp mp.br mq @@ -5894,7 +5886,6 @@ nic.za nichinan.miyazaki.jp nichinan.tottori.jp nico -nid.io nieruchomosci.pl niigata.jp niigata.niigata.jp @@ -6256,7 +6247,6 @@ oncilla.mythic-beasts.com ondigitalocean.app one onfabrica.com -onflashdrive.app ong ong.br onga.fukuoka.jp @@ -6552,6 +6542,8 @@ ozu.ehime.jp ozu.kumamoto.jp p.bg p.se +p.tawk.email +p.tawkto.email pa pa.gov.br pa.gov.pl @@ -6565,7 +6557,6 @@ paas.massivegrid.com padova.it padua.it page -pagefrontapp.com pages.dev pages.gay pages.it.hs-heilbronn.de @@ -6605,7 +6596,6 @@ pb.leg.br pc.it pc.pl pccw -pcloud.host pd.it pdns.page pe @@ -7630,6 +7620,7 @@ servebbs.net servebbs.org servebeer.com serveblog.net +servebolt.cloud servecounterstrike.com serveexchange.com serveftp.com @@ -7678,7 +7669,6 @@ shakotan.hokkaido.jp shangrila shari.hokkaido.jp sharp -shaw sheezy.games shell shia @@ -7690,8 +7680,6 @@ shibukawa.gunma.jp shibuya.tokyo.jp shichikashuku.miyagi.jp shichinohe.aomori.jp -shiftcrypto.dev -shiftcrypto.io shiftedit.io shiga.jp shiiba.miyazaki.jp @@ -8064,7 +8052,6 @@ stuff-4-sale.us stufftoread.com style su -su.paba.se sub.jp sucks sue.fukuoka.jp @@ -8250,6 +8237,7 @@ tattoo tawaramoto.nara.jp tax taxi +taxi.aero taxi.br tayninh.vn tc @@ -9732,6 +9720,7 @@ yoshinogari.saga.jp yoshioka.gunma.jp yotsukaido.chiba.jp you +you2.pl youtube yt yuasa.wakayama.jp diff --git a/core/src/test/java/com/linecorp/armeria/client/ClientRequestContextTest.java b/core/src/test/java/com/linecorp/armeria/client/ClientRequestContextTest.java index be006e0fea2..d549701c386 100644 --- a/core/src/test/java/com/linecorp/armeria/client/ClientRequestContextTest.java +++ b/core/src/test/java/com/linecorp/armeria/client/ClientRequestContextTest.java @@ -19,15 +19,21 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.function.Function; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.ValueSource; import com.linecorp.armeria.common.HttpMethod; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.RequestHeaders; +import com.linecorp.armeria.common.TimeoutException; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.SafeCloseable; import com.linecorp.armeria.server.ServiceRequestContext; @@ -276,6 +282,24 @@ void updateRequestWithInvalidPath(String path) { .hasMessageContaining("invalid path"); } + @ParameterizedTest + @ArgumentsSource(TimedOutExceptionProvider.class) + void isTimedOut_true(Throwable cause) { + final ClientRequestContext cctx = clientRequestContext(); + cctx.cancel(cause); + cctx.whenResponseCancelled().join(); + assertThat(cctx.isTimedOut()).isTrue(); + } + + @ParameterizedTest + @ArgumentsSource(NotTimedOutExceptionProvider.class) + void isTimedOut_false(Throwable cause) { + final ClientRequestContext cctx = clientRequestContext(); + cctx.cancel(cause); + cctx.whenResponseCancelled().join(); + assertThat(cctx.isTimedOut()).isFalse(); + } + private static void assertUnwrapAllCurrentCtx(@Nullable RequestContext ctx) { final RequestContext current = RequestContext.currentOrNull(); if (current == null) { @@ -292,4 +316,25 @@ private static ServiceRequestContext serviceRequestContext() { private static ClientRequestContext clientRequestContext() { return ClientRequestContext.of(HttpRequest.of(HttpMethod.GET, "/")); } + + private static class TimedOutExceptionProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return Stream.of(new TimeoutException(), + ResponseTimeoutException.get(), + UnprocessedRequestException.of(ResponseTimeoutException.get())) + .map(Arguments::of); + } + } + + private static class NotTimedOutExceptionProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return Stream.of(new RuntimeException(), + UnprocessedRequestException.of(new RuntimeException())) + .map(Arguments::of); + } + } } diff --git a/core/src/test/java/com/linecorp/armeria/client/EndpointTest.java b/core/src/test/java/com/linecorp/armeria/client/EndpointTest.java index 4502bcad6c1..e1b6a04633f 100644 --- a/core/src/test/java/com/linecorp/armeria/client/EndpointTest.java +++ b/core/src/test/java/com/linecorp/armeria/client/EndpointTest.java @@ -745,8 +745,8 @@ void attrs() { attrs2.set(key1, "value1-2"); attrs2.set(key3, "value3"); - final Endpoint endpointB = endpoint.withAttrs(attrs.build()); - final Endpoint endpointC = endpointB.withAttrs(attrs2.build()); + final Endpoint endpointB = endpoint.replaceAttrs(attrs.build()); + final Endpoint endpointC = endpointB.replaceAttrs(attrs2.build()); assertThat(endpointB.attr(key1)) .isEqualTo("value1"); @@ -767,10 +767,40 @@ void attrs() { Maps.immutableEntry(key3, "value3")); // Reset attrs with an empty attributes. - final Endpoint newEndpointB = endpointB.withAttrs(Attributes.of()); + final Endpoint newEndpointB = endpointB.replaceAttrs(Attributes.of()); assertThat(newEndpointB.attrs().isEmpty()).isTrue(); - final Endpoint sameEndpoint = endpoint.withAttrs(Attributes.of()); + final Endpoint sameEndpoint = endpoint.replaceAttrs(Attributes.of()); assertThat(sameEndpoint).isSameAs(endpoint); } + + @Test + void withAttrs() { + final AttributeKey key1 = AttributeKey.valueOf("key1"); + final AttributeKey key2 = AttributeKey.valueOf("key2"); + + final Endpoint endpoint = Endpoint.parse("a").withAttrs(Attributes.of(key1, "val1")); + assertThat(endpoint.attrs().attrs()) + .toIterable() + .containsExactlyInAnyOrder(Maps.immutableEntry(key1, "val1")); + + assertThat(endpoint.withAttrs(Attributes.of(key2, "val2")).attrs().attrs()) + .toIterable() + .containsExactlyInAnyOrder(Maps.immutableEntry(key1, "val1"), + Maps.immutableEntry(key2, "val2")); + + assertThat(endpoint.withAttrs(Attributes.of(key1, "val1")).attrs().attrs()) + .toIterable() + .containsExactlyInAnyOrder(Maps.immutableEntry(key1, "val1")); + + assertThat(endpoint.withAttrs(Attributes.of(key1, "val2")).attrs().attrs()) + .toIterable() + .containsExactlyInAnyOrder(Maps.immutableEntry(key1, "val2")); + + assertThat(endpoint.withAttrs(Attributes.of()).attrs().attrs()) + .toIterable() + .containsExactlyInAnyOrder(Maps.immutableEntry(key1, "val1")); + + assertThat(endpoint.withAttrs(Attributes.of())).isSameAs(endpoint); + } } diff --git a/core/src/test/java/com/linecorp/armeria/client/HttpClientFactoryTest.java b/core/src/test/java/com/linecorp/armeria/client/HttpClientFactoryTest.java index 4163e8e52c4..aeb9d55b585 100644 --- a/core/src/test/java/com/linecorp/armeria/client/HttpClientFactoryTest.java +++ b/core/src/test/java/com/linecorp/armeria/client/HttpClientFactoryTest.java @@ -16,21 +16,39 @@ package com.linecorp.armeria.client; +import static com.google.common.collect.ImmutableList.toImmutableList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; +import java.util.concurrent.CompletionException; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import com.google.common.collect.ImmutableMap; + +import com.linecorp.armeria.client.endpoint.dns.TestDnsServer; +import com.linecorp.armeria.common.CommonPools; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.metric.PrometheusMeterRegistries; import com.linecorp.armeria.server.AbstractHttpService; import com.linecorp.armeria.server.Server; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.dns.DatagramDnsQuery; +import io.netty.resolver.ResolvedAddressTypes; +import io.netty.resolver.dns.DnsServerAddressStreamProvider; +import io.netty.resolver.dns.DnsServerAddresses; +import io.netty.util.ReferenceCountUtil; + class HttpClientFactoryTest { @RegisterExtension public static final ServerExtension server = new ServerExtension() { @@ -103,4 +121,61 @@ protected HttpResponse doGet(ServiceRequestContext ctx, }); } } + + @Test + void execute_dnsTimeout_clientRequestContext_isTimedOut() { + try (TestDnsServer dnsServer = new TestDnsServer(ImmutableMap.of(), new AlwaysTimeoutHandler())) { + try (RefreshingAddressResolverGroup group = dnsTimeoutBuilder(dnsServer) + .build(CommonPools.workerGroup().next())) { + final ClientFactory clientFactory = ClientFactory + .builder() + .addressResolverGroupFactory(eventExecutors -> group) + .build(); + final Endpoint endpoint = Endpoint + .of("test") + .withIpAddr(null); // to invoke dns resolve address + final WebClient client = WebClient + .builder(endpoint.toUri(SessionProtocol.H1C)) + .factory(clientFactory) + .build(); + + try (ClientRequestContextCaptor captor = Clients.newContextCaptor()) { + assertThatThrownBy(() -> client.get("/").aggregate().join()) + .isInstanceOf(CompletionException.class) + .hasCauseInstanceOf(UnprocessedRequestException.class) + .hasRootCauseInstanceOf(DnsTimeoutException.class); + captor.get().whenResponseCancelled().join(); + assertThat(captor.get().isTimedOut()).isTrue(); + } + + clientFactory.close(); + endpoint.close(); + } + } + } + + private static class AlwaysTimeoutHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof DatagramDnsQuery) { + // Just release the msg and return so that the client request is timed out. + ReferenceCountUtil.safeRelease(msg); + return; + } + super.channelRead(ctx, msg); + } + } + + private static DnsResolverGroupBuilder dnsTimeoutBuilder(TestDnsServer... servers) { + final DnsServerAddressStreamProvider dnsServerAddressStreamProvider = + hostname -> DnsServerAddresses.sequential( + Stream.of(servers).map(TestDnsServer::addr).collect(toImmutableList())).stream(); + final DnsResolverGroupBuilder builder = new DnsResolverGroupBuilder() + .serverAddressStreamProvider(dnsServerAddressStreamProvider) + .meterRegistry(PrometheusMeterRegistries.newRegistry()) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .traceEnabled(false) + .queryTimeoutMillis(1); // dns timeout + return builder; + } } diff --git a/core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedBeanFactoryRegistryTest.java b/core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedBeanFactoryRegistryTest.java index c494e3cb102..46bff45a66f 100644 --- a/core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedBeanFactoryRegistryTest.java +++ b/core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedBeanFactoryRegistryTest.java @@ -39,8 +39,9 @@ public class AnnotatedBeanFactoryRegistryTest { public static final DependencyInjector noopDependencyInjector = new DependencyInjector() { + @Nullable @Override - public @Nullable T getInstance(Class type) { + public T getInstance(Class type) { return null; } diff --git a/core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTest.java b/core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTest.java index 6da6e322001..e078a605b07 100644 --- a/core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTest.java +++ b/core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceTest.java @@ -773,6 +773,12 @@ public String customHeader5(@Header List numbers, String.join(":", strings); } + @Post("/headerNameSpecified") + public String headerNameSpecified(@Header("X-x-FoO-bAr") String id) { + // Because the header name is specified, it's not converted. + return id; + } + @Get("/headerDefault") public String headerDefault(RequestContext ctx, @Header @Default("hello") String username, @@ -1224,6 +1230,10 @@ void testRequestHeaderInjection() throws Exception { request.addHeader("strings", "minwoox"); testBody(hc, request, "1:2:1/minwoox:giraffe"); + request = post("/11/headerNameSpecified"); + request.addHeader("X-x-FoO-bAr", "qwerty"); + testBody(hc, request, "qwerty"); + request = get("/11/headerDefault"); testBody(hc, request, "hello/world/(null)"); diff --git a/core/src/test/java/com/linecorp/armeria/server/ProtocolViolationHandlingTest.java b/core/src/test/java/com/linecorp/armeria/server/ProtocolViolationHandlingTest.java new file mode 100644 index 00000000000..58fc88eb313 --- /dev/null +++ b/core/src/test/java/com/linecorp/armeria/server/ProtocolViolationHandlingTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.server; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.linecorp.armeria.client.BlockingWebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.MediaType; +import com.linecorp.armeria.common.RequestHeaders; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.annotation.Nullable; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; + +import joptsimple.internal.Strings; + +class ProtocolViolationHandlingTest { + + @RegisterExtension + static ServerExtension server = new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.http1MaxInitialLineLength(100); + sb.service("/", (ctx, req) -> HttpResponse.of(HttpStatus.OK)); + sb.errorHandler(new ServerErrorHandler() { + @Override + public @Nullable HttpResponse onServiceException(ServiceRequestContext ctx, Throwable cause) { + return null; + } + + @Override + public @Nullable AggregatedHttpResponse onProtocolViolation(ServiceConfig config, + @Nullable RequestHeaders headers, + HttpStatus status, + @Nullable String description, + @Nullable Throwable cause) { + return AggregatedHttpResponse.of(HttpStatus.BAD_REQUEST, MediaType.PLAIN_TEXT, + "Custom response"); + } + }); + } + }; + + @Test + void shouldHandleInvalidHttp1Request() { + final BlockingWebClient client = BlockingWebClient.of(server.uri(SessionProtocol.H1C)); + final AggregatedHttpResponse res = client.get("/?" + Strings.repeat('a', 100)); + assertThat(res.status()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(res.contentUtf8()).isEqualTo("Custom response"); + } + + @Test + void shouldHandleInvalidHttp2Request() { + final BlockingWebClient client = BlockingWebClient.of(server.uri(SessionProtocol.H2C)); + final AggregatedHttpResponse res = client.get("*"); + assertThat(res.status()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(res.contentUtf8()).isEqualTo("Custom response"); + } +} diff --git a/core/src/test/java/com/linecorp/armeria/server/ServerMetricsTest.java b/core/src/test/java/com/linecorp/armeria/server/ServerMetricsTest.java index 11b21bcac4a..43106ff8cf3 100644 --- a/core/src/test/java/com/linecorp/armeria/server/ServerMetricsTest.java +++ b/core/src/test/java/com/linecorp/armeria/server/ServerMetricsTest.java @@ -206,7 +206,7 @@ void checkWhenOk(SessionProtocol sessionProtocol, long expectedPendingHttp1Reque assertThat(result.status()).isSameAs(HttpStatus.OK); assertThat(serverMetrics.pendingRequests()).isZero(); - assertThat(serverMetrics.activeRequests()).isZero(); + await().untilAsserted(() -> assertThat(serverMetrics.activeRequests()).isZero()); await().until(() -> serverMetrics.activeConnections() == 0); } } @@ -273,7 +273,7 @@ void checkWhenRequestTimeout(SessionProtocol sessionProtocol, long expectedPendi assertThat(result.status()).isSameAs(HttpStatus.SERVICE_UNAVAILABLE); assertThat(serverMetrics.pendingRequests()).isZero(); - assertThat(serverMetrics.activeRequests()).isZero(); + await().untilAsserted(() -> assertThat(serverMetrics.activeRequests()).isZero()); await().until(() -> serverMetrics.activeConnections() == 0); } } diff --git a/core/src/test/java/com/linecorp/armeria/server/ServiceNamingTest.java b/core/src/test/java/com/linecorp/armeria/server/ServiceNamingTest.java index 51cd472a3cb..d66d839e5d4 100644 --- a/core/src/test/java/com/linecorp/armeria/server/ServiceNamingTest.java +++ b/core/src/test/java/com/linecorp/armeria/server/ServiceNamingTest.java @@ -186,7 +186,7 @@ void shorten_trimTrailingDollarSignOnly() { private static ServiceConfig newServiceConfig(HttpService httpService, ServiceNaming serviceNaming) { return new ServiceConfig(Route.ofCatchAll(), Route.ofCatchAll(), httpService, - null, null, serviceNaming, 0, 0, false, + null, serviceNaming, null, 0, 0, false, AccessLogWriter.common(), CommonPools.blockingTaskExecutor(), SuccessFunction.always(), 0, Files.newTemporaryFolder().toPath(), diff --git a/core/src/test/java/com/linecorp/armeria/server/ServiceTest.java b/core/src/test/java/com/linecorp/armeria/server/ServiceTest.java index 9b67ac5dc3d..f14815c7df3 100644 --- a/core/src/test/java/com/linecorp/armeria/server/ServiceTest.java +++ b/core/src/test/java/com/linecorp/armeria/server/ServiceTest.java @@ -60,8 +60,9 @@ private static void assertDecoration(FooService inner, HttpService outer) throws // Test if FooService.serviceAdded() is invoked. final ServiceConfig cfg = new ServiceConfig(Route.ofCatchAll(), Route.ofCatchAll(), - outer, /* defaultLogName */ null, /* defaultServiceName */ null, - ServiceNaming.of("FooService"), 1, 1, true, + outer, /* defaultServiceName */ null, + ServiceNaming.of("FooService"), /* defaultLogName */ null, + 1, 1, true, AccessLogWriter.disabled(), CommonPools.blockingTaskExecutor(), SuccessFunction.always(), diff --git a/core/src/test/java/com/linecorp/armeria/server/file/FileServiceTest.java b/core/src/test/java/com/linecorp/armeria/server/file/FileServiceTest.java index b686b4a75dd..890599a4687 100644 --- a/core/src/test/java/com/linecorp/armeria/server/file/FileServiceTest.java +++ b/core/src/test/java/com/linecorp/armeria/server/file/FileServiceTest.java @@ -41,6 +41,7 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; @@ -54,8 +55,10 @@ import com.google.common.io.ByteStreams; import com.google.common.io.Resources; +import com.linecorp.armeria.client.BlockingWebClient; import com.linecorp.armeria.client.WebClient; import com.linecorp.armeria.common.AggregatedHttpResponse; +import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.OsType; @@ -173,6 +176,23 @@ protected void configure(ServerBuilder sb) { .maxCacheEntries(0) .build())); + sb.serviceUnder( + "/no-extension", + FileService.builder(classLoader, baseResourceDir + "foo") + .build()); + sb.serviceUnder( + "/extension", + FileService.builder(classLoader, baseResourceDir + "foo") + .fallbackFileExtensions("txt") + .build()); + sb.serviceUnder( + "/extension/decompress", + FileService.builder(classLoader, baseResourceDir + "foo") + .fallbackFileExtensions("txt") + .serveCompressedFiles(true) + .autoDecompress(true) + .build()); + sb.decorator(LoggingService.newDecorator()); } }; @@ -625,6 +645,33 @@ void testFileSystemGetUtf8(String baseUri) throws Exception { } } + @Test + void useFileExtensionsToFindFile() { + final BlockingWebClient client = server.blockingWebClient(); + AggregatedHttpResponse response = client.get("/extension/foo.txt"); + assertThat(response.status()).isEqualTo(HttpStatus.OK); + assertThat(response.contentUtf8()).isEqualTo("foo"); + + // Without .txt extension + response = client.get("/extension/foo"); + assertThat(response.status()).isEqualTo(HttpStatus.OK); + assertThat(response.contentUtf8()).isEqualTo("foo"); + // Make sure that the existing operation is not affected by the fileExtensions option. + response = client.get("/extension/"); + assertThat(response.status()).isEqualTo(HttpStatus.OK); + assertThat(response.contentUtf8()).isEqualTo("\n"); + + response = client.get("/no-extension/foo.txt"); + assertThat(response.status()).isEqualTo(HttpStatus.OK); + assertThat(response.contentUtf8()).isEqualTo("foo"); + response = client.get("/no-extension/foo"); + assertThat(response.status()).isEqualTo(HttpStatus.NOT_FOUND); + + response = client.get("/extension/decompress/foo"); + assertThat(response.status()).isEqualTo(HttpStatus.OK); + assertThat(response.contentUtf8()).isEqualTo("foo"); + } + private static void writeFile(Path path, String content) throws Exception { // Retry to work around the `AccessDeniedException` in Windows. for (int i = 9; i >= 0; i--) { diff --git a/dependencies.toml b/dependencies.toml index a4b19016f16..c32a67d6068 100644 --- a/dependencies.toml +++ b/dependencies.toml @@ -98,6 +98,8 @@ netty = "4.1.110.Final" netty-incubator-transport-native-io_uring = "0.0.25.Final" nexus-publish = "2.0.0" node-gradle-plugin = "7.0.2" +nullaway = "0.11.0" +nullaway-gradle-plugin = "2.0.0" okhttp2 = "2.7.5" # For testing okhttp3 = { strictly = "3.14.9" } # Not just for testing. Used in the Retrofit mudule. okhttp4 = "4.12.0" # For testing @@ -159,6 +161,8 @@ thrift015 = { strictly = "0.15.0" } thrift016 = { strictly = "0.16.0" } thrift017 = { strictly = "0.17.0" } thrift018 = { strictly = "0.18.1" } +thrift019 = { strictly = "0.19.0" } +thrift020 = { strictly = "0.20.0" } tomcat8 = "8.5.100" tomcat9 = "9.0.87" tomcat10 = "10.1.20" @@ -943,6 +947,10 @@ module = 'io.netty:netty-tcnative-boringssl-static' module = "io.netty.incubator:netty-incubator-transport-native-io_uring" version.ref = "netty-incubator-transport-native-io_uring" +[libraries.nullaway] +module = "com.uber.nullaway:nullaway" +version.ref = "nullaway" + [libraries.picocli] module = "info.picocli:picocli" version.ref = "picocli" @@ -1329,6 +1337,23 @@ exclusions = [ "javax.annotation:javax.annotation-api", "org.apache.httpcomponents:httpcore", "org.apache.httpcomponents:httpclient"] +[libraries.thrift019] +module = "org.apache.thrift:libthrift" +version.ref = "thrift019" +javadocs = "https://www.javadoc.io/doc/org.apache.thrift/libthrift/0.19.0/" +exclusions = [ + "javax.annotation:javax.annotation-api", + "org.apache.httpcomponents:httpcore", + "org.apache.httpcomponents:httpclient"] +[libraries.thrift020] +module = "org.apache.thrift:libthrift" +version.ref = "thrift020" +javadocs = "https://www.javadoc.io/doc/org.apache.thrift/libthrift/0.20.0/" +exclusions = [ + "javax.annotation:javax.annotation-api", + "org.apache.httpcomponents:httpcore", + "org.apache.httpcomponents:httpclient"] + [libraries.tomcat8-core] module = "org.apache.tomcat.embed:tomcat-embed-core" @@ -1396,15 +1421,16 @@ module = "io.github.resilience4j:resilience4j-micrometer" version.ref = "resilience4j" [plugins] +errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-gradle-plugin" } jkube = { id = "org.eclipse.jkube.kubernetes", version.ref = "jkube" } jmh = { id = "me.champeau.jmh", version.ref = "jmh-gradle-plugin" } -osdetector = { id = "com.google.osdetector", version.ref = "osdetector" } kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" } kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } -scalafmt = { id = "cz.alenkacz.gradle.scalafmt", version.ref = "scalafmt-gradle-plugin" } -nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus-publish" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle-plugin" } -errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-gradle-plugin" } +nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus-publish" } node-gradle = { id = "com.github.node-gradle.node", version.ref = "node-gradle-plugin" } +nullaway = { id = "net.ltgt.nullaway", version.ref = "nullaway-gradle-plugin" } +osdetector = { id = "com.google.osdetector", version.ref = "osdetector" } +scalafmt = { id = "cz.alenkacz.gradle.scalafmt", version.ref = "scalafmt-gradle-plugin" } spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot2" } diff --git a/docs-client/build.gradle b/docs-client/build.gradle index 4a218786b8b..447c597b549 100644 --- a/docs-client/build.gradle +++ b/docs-client/build.gradle @@ -11,15 +11,10 @@ if (rootProject.hasProperty('noWeb')) { } node { - version = '16.19.1' + version = '22.3.0' + npmVersion = '10.8.1' download = true npmInstallCommand = "ci" - - // Change the cache location under Gradle user home directory so that it's cached by CI. - if (System.getenv('CI') != null) { - workDir = file("${gradle.gradleUserHomeDir}/caches/nodejs/${project.name}") - npmWorkDir = file("${gradle.gradleUserHomeDir}/caches/npm/${project.name}") - } } // Add the option that works around the dependency conflicts. diff --git a/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ArmeriaConfigurationUtil.java b/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ArmeriaConfigurationUtil.java index 6559c47e698..1833209b6ca 100644 --- a/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ArmeriaConfigurationUtil.java +++ b/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ArmeriaConfigurationUtil.java @@ -127,7 +127,10 @@ static void configureServer(ServerBuilder serverBuilder, ArmeriaSettings setting } if (settings.getPorts().isEmpty()) { - serverBuilder.port(new ServerPort(DEFAULT_PORT.getPort(), DEFAULT_PORT.getProtocols())); + final int port = DEFAULT_PORT.getPort(); + final List protocols = DEFAULT_PORT.getProtocols(); + assert protocols != null; + serverBuilder.port(new ServerPort(port, protocols)); } else { configurePorts(serverBuilder, settings.getPorts()); } @@ -445,8 +448,9 @@ private static void configureAccessLog(ServerBuilder serverBuilder, AccessLog ac } else if ("combined".equals(accessLog.getType())) { serverBuilder.accessLogWriter(AccessLogWriter.combined(), true); } else if ("custom".equals(accessLog.getType())) { - serverBuilder - .accessLogWriter(AccessLogWriter.custom(accessLog.getFormat()), true); + final String format = accessLog.getFormat(); + assert format != null; + serverBuilder.accessLogWriter(AccessLogWriter.custom(format), true); } } diff --git a/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ArmeriaServerFactory.java b/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ArmeriaServerFactory.java index c448f63e881..6bf4868c3b2 100644 --- a/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ArmeriaServerFactory.java +++ b/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ArmeriaServerFactory.java @@ -64,6 +64,7 @@ class ArmeriaServerFactory extends AbstractServerFactory { public static final String TYPE = "armeria"; private static final Logger logger = LoggerFactory.getLogger(ArmeriaServerFactory.class); + @Nullable @JsonUnwrapped private @Valid ArmeriaSettings armeriaSettings; @@ -80,6 +81,7 @@ class ArmeriaServerFactory extends AbstractServerFactory { @JsonProperty private boolean jerseyEnabled = true; + @Nullable @JsonIgnore public ServerBuilder getServerBuilder() { return serverBuilder; @@ -184,6 +186,7 @@ private ScheduledThreadPoolExecutor newBlockingTaskExecutor() { return blockingTaskExecutor; } + @Nullable ArmeriaSettings getArmeriaSettings() { return armeriaSettings; } diff --git a/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ManagedArmeriaServer.java b/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ManagedArmeriaServer.java index 04aaa97f271..a031b6d27c1 100644 --- a/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ManagedArmeriaServer.java +++ b/dropwizard2/src/main/java/com/linecorp/armeria/dropwizard/ManagedArmeriaServer.java @@ -65,6 +65,7 @@ class ManagedArmeriaServer implements Managed { public void start() throws Exception { logger.trace("Getting Armeria Server Builder"); final ServerBuilder sb = ((ArmeriaServerFactory) serverFactory).getServerBuilder(); + assert sb != null; logger.trace("Calling Server Configurator"); serverConfigurator.configure(sb); server = sb.build(); diff --git a/eureka/src/main/java/com/linecorp/armeria/client/eureka/EurekaEndpointGroup.java b/eureka/src/main/java/com/linecorp/armeria/client/eureka/EurekaEndpointGroup.java index d615d758da1..631817fd1a7 100644 --- a/eureka/src/main/java/com/linecorp/armeria/client/eureka/EurekaEndpointGroup.java +++ b/eureka/src/main/java/com/linecorp/armeria/client/eureka/EurekaEndpointGroup.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.net.URI; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.CompletableFuture; @@ -284,7 +285,7 @@ private static Function> responseConverter( } else if (appName != null) { filter = instanceInfo -> appName.equals(instanceInfo.getAppName()); } else { - filter = instanceInfo -> instanceId.equals(instanceInfo.getInstanceId()); + filter = instanceInfo -> Objects.equals(instanceId, instanceInfo.getInstanceId()); } } final StringJoiner joiner = new StringJoiner(","); diff --git a/eureka/src/main/java/com/linecorp/armeria/internal/common/eureka/InstanceInfo.java b/eureka/src/main/java/com/linecorp/armeria/internal/common/eureka/InstanceInfo.java index a8edb005be8..22d18108f1a 100644 --- a/eureka/src/main/java/com/linecorp/armeria/internal/common/eureka/InstanceInfo.java +++ b/eureka/src/main/java/com/linecorp/armeria/internal/common/eureka/InstanceInfo.java @@ -43,6 +43,7 @@ public final class InstanceInfo { private static final Logger logger = LoggerFactory.getLogger(InstanceInfo.class); + @Nullable private final String instanceId; @Nullable diff --git a/eureka/src/main/java/com/linecorp/armeria/server/eureka/EurekaUpdatingListener.java b/eureka/src/main/java/com/linecorp/armeria/server/eureka/EurekaUpdatingListener.java index 712c044b3ba..d5f1a93f13b 100644 --- a/eureka/src/main/java/com/linecorp/armeria/server/eureka/EurekaUpdatingListener.java +++ b/eureka/src/main/java/com/linecorp/armeria/server/eureka/EurekaUpdatingListener.java @@ -133,6 +133,7 @@ public static EurekaUpdatingListenerBuilder builder( private final EurekaWebClient client; private final InstanceInfo initialInstanceInfo; + @Nullable private InstanceInfo instanceInfo; @Nullable private volatile ScheduledFuture heartBeatFuture; @@ -335,8 +336,9 @@ public void serverStopping(Server server) throws Exception { if (heartBeatFuture != null) { heartBeatFuture.cancel(false); } + final InstanceInfo instanceInfo = this.instanceInfo; final String appName = this.appName; - if (appName != null) { + if (instanceInfo != null && appName != null) { final String instanceId = instanceInfo.getInstanceId(); assert instanceId != null; client.cancel(appName, instanceId).aggregate().handle((res, cause) -> { diff --git a/gradle.properties b/gradle.properties index bcc699d8a57..f230d5fcd73 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,4 +27,5 @@ org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError systemProp.https.protocols=TLSv1,TLSv1.1,TLSv1.2 jacocoExclusions=com/linecorp/armeria/internal/common/CurrentJavaVersionSpecific,com/linecorp/armeria/*/scalapb/**,META-INF/versions/** +shadowExclusions=META-INF/services/java.security.Provider org.gradle.caching = true diff --git a/gradle/scripts/.github/CODEOWNERS b/gradle/scripts/.github/CODEOWNERS deleted file mode 100644 index 2e6aa5b41eb..00000000000 --- a/gradle/scripts/.github/CODEOWNERS +++ /dev/null @@ -1,4 +0,0 @@ -# https://help.github.com/articles/about-codeowners/ - -* @jrhee17 @ikhoon @minwoox @trustin - diff --git a/gradle/scripts/.gitrepo b/gradle/scripts/.gitrepo index bd6cfd448cc..d770e09d55f 100644 --- a/gradle/scripts/.gitrepo +++ b/gradle/scripts/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/line/gradle-scripts branch = main - commit = a3211a7ec874b42fc7dc5a84b3960a705d5fc34c - parent = c66b9211afdd86ce388b5b77b99fc7ffad6c0888 + commit = 1f94acd56f170782ad291e4603384ad59cca4e9e + parent = d18437e44118f1367ce3cf2d7e5008552ebd7513 method = merge - cmdver = 0.4.6 + cmdver = 0.4.5 diff --git a/gradle/scripts/README.md b/gradle/scripts/README.md index 29bc8c81f0b..f8a4e54a6c4 100644 --- a/gradle/scripts/README.md +++ b/gradle/scripts/README.md @@ -34,6 +34,7 @@ sensible defaults. By applying them, you can: - [Shading a multi-module project with `relocate` flag](#shading-a-multi-module-project-with-relocate-flag) - [Setting a Java target version with the `java(\\d+)` flag](#setting-a-java-target-version-with-the-javad-flag) - [Setting a Kotlin target version with the `kotlin(\\d+\\.\\d+)` flag](#setting-a-koltin-target-version-with-the-kotlindd-flag) +- [Automatic module names](#automatic-module-names) - [Tagging conveniently with `release` task](#tagging-conveniently-with-release-task) @@ -98,6 +99,7 @@ sensible defaults. By applying them, you can: ``` group=com.doe.john.myexample version=0.0.1-SNAPSHOT + versionPattern=^[0-9]+\\.[0-9]+\\.[0-9]+$ projectName=My Example projectUrl=https://www.example.com/ projectDescription=My example project @@ -118,6 +120,7 @@ sensible defaults. By applying them, you can: googleAnalyticsId=UA-XXXXXXXX javaSourceCompatibility=1.8 javaTargetCompatibility=1.8 + automaticModuleNames=false ``` 5. That's all. You now have two Java subprojects with sensible defaults. @@ -576,6 +579,10 @@ relocations [ { from: "com.google.common", to: "com.doe.john.myproject.shaded.gu Unshaded tests are disabled by default when a shading task is configured. If you want to run unshaded tests, you can specify `-PpreferShadedTests=false` option. + +If you would like to remove specific files when shading the JAR, you may specify the +`-PshadowExclusions=` option. + ### Trimming a shaded JAR with `trim` flag If you shade many dependencies, your JAR will grow huge, even if you only use @@ -675,6 +682,31 @@ However, if you want to compile a Kotlin module with a different language versio For example, `kotlin1.6` flag makes your Kotlin module compatible with language version 1.6 and API version 1.6. +## Automatic module names + +By specifying the `automaticModuleNames=true` property in `settings.gradle`, every `java` project's JAR +file will contain the `Automatic-Module-Name` property in its `MANIFEST.MF`, auto-generated from the group ID +and artifact ID. For example: + +- groupId: `com.example`, artifactId: `foo-bar` + - module name: `com.example.foo.bar` +- groupId: `com.example.foo`, artifactId: `foo-bar` + - module name: `com.example.foo.bar` + +If enabled, each project with `java` flag will have the `automaticModuleName` property. + +You can override the automatic module name of a certain project via the `automaticModuleNameOverrides` +extension property: + + ```groovy + ext { + // Change the automatic module name of project ':bar' to 'com.example.fubar'. + automaticModuleNameOverrides = [ + ':bar': 'com.example.fubar' + ] + } + ``` + ## Tagging conveniently with `release` task The task called `release` is added at the top level project. It will update the diff --git a/gradle/scripts/lib/common-info.gradle b/gradle/scripts/lib/common-info.gradle index 13666f21cf7..0f29c25407e 100644 --- a/gradle/scripts/lib/common-info.gradle +++ b/gradle/scripts/lib/common-info.gradle @@ -42,17 +42,9 @@ allprojects { ext { artifactId = { // Use the overridden one if available. - if (rootProject.ext.has('artifactIdOverrides')) { - def overrides = rootProject.ext.artifactIdOverrides - if (!(overrides instanceof Map)) { - throw new IllegalStateException("artifactIdOverrides must be a Map: ${overrides}") - } - - for (Map.Entry e : overrides.entrySet()) { - if (rootProject.project(e.key) == project) { - return e.value - } - } + def overriddenArtifactId = findOverridden('artifactIdOverrides', project) + if (overriddenArtifactId != null) { + return overriddenArtifactId } // Generate from the project names otherwise. @@ -70,3 +62,49 @@ allprojects { }.call() } } + +// Check whether to enable automatic module names. +def isAutomaticModuleNameEnabled = 'true' == rootProject.findProperty('automaticModuleNames') + +allprojects { + ext { + automaticModuleName = { + if (!isAutomaticModuleNameEnabled) { + return null + } + + // Use the overridden one if available. + def overriddenAutomaticModuleName = findOverridden('automaticModuleNameOverrides', project) + if (overriddenAutomaticModuleName != null) { + return overriddenAutomaticModuleName + } + + // Generate from the groupId and artifactId otherwise. + def groupIdComponents = String.valueOf(rootProject.group).split("\\.").toList() + def artifactIdComponents = + String.valueOf(project.ext.artifactId).replace('-', '.').split("\\.").toList() + if (groupIdComponents.last() == artifactIdComponents.first()) { + return String.join('.', groupIdComponents + artifactIdComponents.drop(1)) + } else { + return String.join('.', groupIdComponents + artifactIdComponents) + } + }.call() + } +} + +def findOverridden(String overridesPropertyName, Project project) { + if (rootProject.ext.has(overridesPropertyName)) { + def overrides = rootProject.ext.get(overridesPropertyName) + if (!(overrides instanceof Map)) { + throw new IllegalStateException("rootProject.ext.${overridesPropertyName} must be a Map: ${overrides}") + } + + for (Map.Entry e : overrides.entrySet()) { + if (rootProject.project(e.key) == project) { + return String.valueOf(e.value) + } + } + } + + return null +} diff --git a/gradle/scripts/lib/java-shade.gradle b/gradle/scripts/lib/java-shade.gradle index 061b1304470..05c49d16de3 100644 --- a/gradle/scripts/lib/java-shade.gradle +++ b/gradle/scripts/lib/java-shade.gradle @@ -27,12 +27,31 @@ configure(relocatedProjects) { configureShadowTask(project, delegate, true) archiveBaseName.set("${project.archivesBaseName}-shaded") + // Exclude the legacy file listing. + exclude '/META-INF/INDEX.LIST' // Exclude the class signature files. exclude '/META-INF/*.SF' exclude '/META-INF/*.DSA' exclude '/META-INF/*.RSA' // Exclude the files generated by Maven exclude '/META-INF/maven/**' + // Exclude the module metadata that'll become invalid after relocation. + exclude '**/module-info.class' + + def shadowExclusions = [] + if (rootProject.hasProperty('shadowExclusions')) { + shadowExclusions = rootProject.findProperty('shadowExclusions').split(",") + } + shadowExclusions.each { + exclude it + } + + // Set the 'Automatic-Module-Name' property in MANIFEST.MF. + if (project.ext.automaticModuleName != null) { + manifest { + attributes('Automatic-Module-Name': project.ext.automaticModuleName) + } + } } tasks.assemble.dependsOn tasks.shadedJar diff --git a/gradle/scripts/lib/java.gradle b/gradle/scripts/lib/java.gradle index b9ebc8cae0d..a70498ac852 100644 --- a/gradle/scripts/lib/java.gradle +++ b/gradle/scripts/lib/java.gradle @@ -1,5 +1,6 @@ import java.util.regex.Pattern +// Determine which version of JDK should be used for builds. def buildJdkVersion = Integer.parseInt(JavaVersion.current().getMajorVersion()) if (rootProject.hasProperty('buildJdkVersion')) { def jdkVersion = Integer.parseInt(String.valueOf(rootProject.findProperty('buildJdkVersion'))) @@ -141,6 +142,12 @@ configure(projectsWithFlags('java')) { registerFeature('optional') { usingSourceSet(sourceSets.main) } + + // Do not let Gradle infer the module path if automatic module name is enabled, + // because it means the JAR will rely on JDK's automatic module metadata generation. + if (project.ext.automaticModuleName != null) { + modularity.inferModulePath = false + } } // Set the sensible compiler options. @@ -156,6 +163,15 @@ configure(projectsWithFlags('java')) { options.compilerArgs += '-parameters' } + // Set the 'Automatic-Module-Name' property in 'MANIFEST.MF' if `automaticModuleName` is not null. + if (project.ext.automaticModuleName != null) { + tasks.named('jar') { + manifest { + attributes('Automatic-Module-Name': project.ext.automaticModuleName) + } + } + } + project.ext.configureFlakyTests = { Test testTask -> def flakyTests = rootProject.findProperty('flakyTests') if (flakyTests == 'true') { diff --git a/gradle/scripts/lib/prerequisite.gradle b/gradle/scripts/lib/prerequisite.gradle index 7a94d9b4f8e..4d6adea3999 100644 --- a/gradle/scripts/lib/prerequisite.gradle +++ b/gradle/scripts/lib/prerequisite.gradle @@ -9,12 +9,15 @@ plugins { ''') } -['projectName', 'projectUrl', 'inceptionYear', 'licenseName', 'licenseUrl', 'scmUrl', 'scmConnection', +['group', 'version', 'projectName', 'projectUrl', 'inceptionYear', 'licenseName', 'licenseUrl', 'scmUrl', 'scmConnection', 'scmDeveloperConnection', 'publishUrlForRelease', 'publishUrlForSnapshot', 'publishUsernameProperty', 'publishPasswordProperty'].each { if (rootProject.findProperty(it) == null) { throw new IllegalStateException('''Add project info properties to gradle.properties: +group=com.doe.john.myexample +version=0.0.1-SNAPSHOT +versionPattern=^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$ projectName=My Example projectUrl=https://www.example.com/ projectDescription=My example project @@ -31,7 +34,11 @@ publishUrlForRelease=https://oss.sonatype.org/service/local/staging/deploy/maven publishUrlForSnapshot=https://oss.sonatype.org/content/repositories/snapshots/ publishUsernameProperty=ossrhUsername publishPasswordProperty=ossrhPassword -versionPattern=^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$ +publishSignatureRequired=true +googleAnalyticsId=UA-XXXXXXXX +javaSourceCompatibility=1.8 +javaTargetCompatibility=1.8 +automaticModuleNames=false ''') } } diff --git a/gradle/scripts/lib/thrift/0.19/thrift.linux-aarch_64 b/gradle/scripts/lib/thrift/0.19/thrift.linux-aarch_64 new file mode 100755 index 00000000000..be76cbc7db2 Binary files /dev/null and b/gradle/scripts/lib/thrift/0.19/thrift.linux-aarch_64 differ diff --git a/gradle/scripts/lib/thrift/0.19/thrift.linux-x86_64 b/gradle/scripts/lib/thrift/0.19/thrift.linux-x86_64 new file mode 100755 index 00000000000..41199747949 Binary files /dev/null and b/gradle/scripts/lib/thrift/0.19/thrift.linux-x86_64 differ diff --git a/gradle/scripts/lib/thrift/0.19/thrift.osx-aarch_64 b/gradle/scripts/lib/thrift/0.19/thrift.osx-aarch_64 new file mode 100755 index 00000000000..55e0548d2f0 Binary files /dev/null and b/gradle/scripts/lib/thrift/0.19/thrift.osx-aarch_64 differ diff --git a/gradle/scripts/lib/thrift/0.19/thrift.osx-x86_64 b/gradle/scripts/lib/thrift/0.19/thrift.osx-x86_64 new file mode 100755 index 00000000000..97d766d490b Binary files /dev/null and b/gradle/scripts/lib/thrift/0.19/thrift.osx-x86_64 differ diff --git a/gradle/scripts/lib/thrift/0.19/thrift.windows-x86_64.exe b/gradle/scripts/lib/thrift/0.19/thrift.windows-x86_64.exe new file mode 100755 index 00000000000..ff12a4318d9 Binary files /dev/null and b/gradle/scripts/lib/thrift/0.19/thrift.windows-x86_64.exe differ diff --git a/gradle/scripts/lib/thrift/0.20/thrift.linux-aarch_64 b/gradle/scripts/lib/thrift/0.20/thrift.linux-aarch_64 new file mode 100755 index 00000000000..66f6be8320d Binary files /dev/null and b/gradle/scripts/lib/thrift/0.20/thrift.linux-aarch_64 differ diff --git a/gradle/scripts/lib/thrift/0.20/thrift.linux-x86_64 b/gradle/scripts/lib/thrift/0.20/thrift.linux-x86_64 new file mode 100755 index 00000000000..d47f6b0d827 Binary files /dev/null and b/gradle/scripts/lib/thrift/0.20/thrift.linux-x86_64 differ diff --git a/gradle/scripts/lib/thrift/0.20/thrift.osx-aarch_64 b/gradle/scripts/lib/thrift/0.20/thrift.osx-aarch_64 new file mode 100755 index 00000000000..a8849dcaac3 Binary files /dev/null and b/gradle/scripts/lib/thrift/0.20/thrift.osx-aarch_64 differ diff --git a/gradle/scripts/lib/thrift/0.20/thrift.osx-x86_64 b/gradle/scripts/lib/thrift/0.20/thrift.osx-x86_64 new file mode 100755 index 00000000000..1b75a814dc2 Binary files /dev/null and b/gradle/scripts/lib/thrift/0.20/thrift.osx-x86_64 differ diff --git a/gradle/scripts/lib/thrift/0.20/thrift.windows-x86_64.exe b/gradle/scripts/lib/thrift/0.20/thrift.windows-x86_64.exe new file mode 100755 index 00000000000..a51c13677e0 Binary files /dev/null and b/gradle/scripts/lib/thrift/0.20/thrift.windows-x86_64.exe differ diff --git a/gradle/scripts/lib/thrift/dockerfile/Dockerfile.bionic b/gradle/scripts/lib/thrift/dockerfile/Dockerfile.bionic index f62a6611f90..96dc6c52635 100644 --- a/gradle/scripts/lib/thrift/dockerfile/Dockerfile.bionic +++ b/gradle/scripts/lib/thrift/dockerfile/Dockerfile.bionic @@ -5,7 +5,7 @@ FROM ubuntu:bionic as builder ENV DEBIAN_FRONTEND=noninteractive # see https://archive.apache.org/dist/thrift/ -ENV THRIFT_VERSION=0.18.1 +ENV THRIFT_VERSION=0.20.0 # see https://thrift.apache.org/docs/install/debian.html RUN apt-get update -yq && \ @@ -25,7 +25,7 @@ RUN apt-get update -yq && \ ADD https://archive.apache.org/dist/thrift/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}.tar.gz /tmp/thrift.tar.gz -RUN echo "04c6f10e5d788ca78e13ee2ef0d2152c7b070c0af55483d6b942e29cff296726 /tmp/thrift.tar.gz" | sha256sum -c && \ +RUN echo "b5d8311a779470e1502c027f428a1db542f5c051c8e1280ccd2163fa935ff2d6 /tmp/thrift.tar.gz" | sha256sum -c && \ tar xzf /tmp/thrift.tar.gz -C /tmp RUN cd /tmp/thrift-${THRIFT_VERSION} && \ @@ -38,13 +38,13 @@ RUN cd /tmp/thrift-${THRIFT_VERSION} && \ make -j$(nproc) && \ make install +# Minimizing Thrift binary size +RUN /usr/bin/strip /usr/local/bin/thrift + FROM ubuntu:bionic COPY --from=builder /usr/local/bin/thrift /usr/local/bin/thrift -# Minimizing Thrift binary size -RUN /usr/bin/strip /usr/local/bin/thrift - ENTRYPOINT [ "/usr/local/bin/thrift" ] CMD [ "-help" ] diff --git a/gradle/scripts/lib/thrift/dockerfile/Dockerfile.centos7 b/gradle/scripts/lib/thrift/dockerfile/Dockerfile.centos7 index 5d9fccb3cb6..e968b73b16f 100644 --- a/gradle/scripts/lib/thrift/dockerfile/Dockerfile.centos7 +++ b/gradle/scripts/lib/thrift/dockerfile/Dockerfile.centos7 @@ -6,7 +6,7 @@ FROM centos:centos7 as builder ENV DEBIAN_FRONTEND=noninteractive # see https://archive.apache.org/dist/thrift/ -ENV THRIFT_VERSION=0.18.1 +ENV THRIFT_VERSION=0.20.0 # see https://thrift.apache.org/docs/install/centos.html RUN yum update -y -q && \ @@ -26,7 +26,7 @@ RUN yum update -y -q && \ ADD https://archive.apache.org/dist/thrift/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}.tar.gz /tmp/thrift.tar.gz -RUN echo "04c6f10e5d788ca78e13ee2ef0d2152c7b070c0af55483d6b942e29cff296726 /tmp/thrift.tar.gz" | sha256sum -c && \ +RUN echo "b5d8311a779470e1502c027f428a1db542f5c051c8e1280ccd2163fa935ff2d6 /tmp/thrift.tar.gz" | sha256sum -c && \ tar xzf /tmp/thrift.tar.gz -C /tmp RUN cd /tmp/thrift-${THRIFT_VERSION} && \ @@ -39,13 +39,13 @@ RUN cd /tmp/thrift-${THRIFT_VERSION} && \ make -j$(nproc) && \ make install +# Minimizing Thrift binary size +RUN strip /usr/local/bin/thrift + FROM centos:centos7 COPY --from=builder /usr/local/bin/thrift /usr/local/bin/thrift -# Minimizing Thrift binary size -RUN strip /usr/local/bin/thrift - ENTRYPOINT [ "/usr/local/bin/thrift" ] CMD [ "-help" ] diff --git a/gradle/scripts/lib/thrift/dockerfile/Dockerfile.trusty b/gradle/scripts/lib/thrift/dockerfile/Dockerfile.trusty index 85eba82db44..c7b81ed853e 100644 --- a/gradle/scripts/lib/thrift/dockerfile/Dockerfile.trusty +++ b/gradle/scripts/lib/thrift/dockerfile/Dockerfile.trusty @@ -5,7 +5,7 @@ FROM ubuntu:trusty as builder ENV DEBIAN_FRONTEND=noninteractive # see https://archive.apache.org/dist/thrift/ -ENV THRIFT_VERSION=0.18.1 +ENV THRIFT_VERSION=0.20.0 # see https://thrift.apache.org/docs/install/debian.html RUN apt-get update -yq && \ @@ -25,7 +25,7 @@ RUN apt-get update -yq && \ ADD https://archive.apache.org/dist/thrift/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}.tar.gz /tmp/thrift.tar.gz -RUN echo "04c6f10e5d788ca78e13ee2ef0d2152c7b070c0af55483d6b942e29cff296726 /tmp/thrift.tar.gz" | sha256sum -c && \ +RUN echo "b5d8311a779470e1502c027f428a1db542f5c051c8e1280ccd2163fa935ff2d6 /tmp/thrift.tar.gz" | sha256sum -c && \ tar xzf /tmp/thrift.tar.gz -C /tmp RUN cd /tmp/thrift-${THRIFT_VERSION} && \ @@ -38,13 +38,13 @@ RUN cd /tmp/thrift-${THRIFT_VERSION} && \ make -j$(nproc) && \ make install +# Minimizing Thrift binary size +RUN /usr/bin/strip /usr/local/bin/thrift + FROM ubuntu:trusty COPY --from=builder /usr/local/bin/thrift /usr/local/bin/thrift -# Minimizing Thrift binary size -RUN /usr/bin/strip /usr/local/bin/thrift - ENTRYPOINT [ "/usr/local/bin/thrift" ] CMD [ "-help" ] diff --git a/graphql-protocol/src/main/java/com/linecorp/armeria/common/graphql/protocol/DefaultGraphqlRequest.java b/graphql-protocol/src/main/java/com/linecorp/armeria/common/graphql/protocol/DefaultGraphqlRequest.java index b102d5e54a0..c8139d6ae6c 100644 --- a/graphql-protocol/src/main/java/com/linecorp/armeria/common/graphql/protocol/DefaultGraphqlRequest.java +++ b/graphql-protocol/src/main/java/com/linecorp/armeria/common/graphql/protocol/DefaultGraphqlRequest.java @@ -44,6 +44,7 @@ public String query() { return query; } + @Nullable @Override public String operationName() { return operationName; diff --git a/graphql/src/main/java/com/linecorp/armeria/server/graphql/DefaultGraphqlErrorHandler.java b/graphql/src/main/java/com/linecorp/armeria/server/graphql/DefaultGraphqlErrorHandler.java index 33fde0a1242..b73326f1039 100644 --- a/graphql/src/main/java/com/linecorp/armeria/server/graphql/DefaultGraphqlErrorHandler.java +++ b/graphql/src/main/java/com/linecorp/armeria/server/graphql/DefaultGraphqlErrorHandler.java @@ -22,6 +22,8 @@ import javax.annotation.Nonnull; +import com.google.common.collect.ImmutableMap; + import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.common.MediaType; @@ -42,7 +44,7 @@ enum DefaultGraphqlErrorHandler implements GraphqlErrorHandler { public HttpResponse handle( ServiceRequestContext ctx, ExecutionInput input, - ExecutionResult result, + @Nullable ExecutionResult result, @Nullable Throwable cause) { final MediaType produceType = GraphqlUtil.produceType(ctx.request().headers()); @@ -60,11 +62,11 @@ public HttpResponse handle( return HttpResponse.ofJson(HttpStatus.INTERNAL_SERVER_ERROR, produceType, specification); } - if (result.getErrors().stream().anyMatch(ValidationError.class::isInstance)) { + if (result != null && result.getErrors().stream().anyMatch(ValidationError.class::isInstance)) { return HttpResponse.ofJson(HttpStatus.BAD_REQUEST, produceType, result.toSpecification()); } - return HttpResponse.ofJson(produceType, result.toSpecification()); + return HttpResponse.ofJson(produceType, result != null ? result.toSpecification() : ImmutableMap.of()); } private static Map toSpecification(Throwable cause) { diff --git a/graphql/src/main/java/com/linecorp/armeria/server/graphql/DefaultGraphqlService.java b/graphql/src/main/java/com/linecorp/armeria/server/graphql/DefaultGraphqlService.java index ee54bd59984..d1edffc6a35 100644 --- a/graphql/src/main/java/com/linecorp/armeria/server/graphql/DefaultGraphqlService.java +++ b/graphql/src/main/java/com/linecorp/armeria/server/graphql/DefaultGraphqlService.java @@ -34,6 +34,7 @@ import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.graphql.protocol.GraphqlRequest; +import com.linecorp.armeria.common.util.Exceptions; import com.linecorp.armeria.internal.server.graphql.protocol.GraphqlUtil; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.server.graphql.protocol.AbstractGraphqlService; @@ -49,7 +50,7 @@ final class DefaultGraphqlService extends AbstractGraphqlService implements Grap private final GraphQL graphQL; private final Function dataLoaderRegistryFunction; + ? extends DataLoaderRegistry> dataLoaderRegistryFunction; private final boolean useBlockingTaskExecutor; @@ -111,25 +112,37 @@ public CompletableFuture executeGraphql(ServiceRequestContext c private HttpResponse execute( ServiceRequestContext ctx, ExecutionInput input, MediaType produceType) { - final CompletableFuture future = executeGraphql(ctx, input); - return HttpResponse.of( - future.handle((executionResult, cause) -> { - if (executionResult.getData() instanceof Publisher) { - logger.warn("executionResult.getData() returns a {} that is not supported yet.", - executionResult.getData().toString()); - - return HttpResponse.ofJson(HttpStatus.NOT_IMPLEMENTED, - produceType, - toSpecification( - "Use GraphQL over WebSocket for subscription")); - } - - if (executionResult.getErrors().isEmpty() && cause == null) { - return HttpResponse.ofJson(produceType, executionResult.toSpecification()); - } - - return errorHandler.handle(ctx, input, executionResult, cause); - })); + try { + final CompletableFuture future = executeGraphql(ctx, input); + return HttpResponse.of( + future.handle((executionResult, cause) -> { + if (cause != null) { + cause = Exceptions.peel(cause); + return errorHandler.handle(ctx, input, null, cause); + } + + if (executionResult.getData() instanceof Publisher) { + logger.warn("Use GraphQL over WebSocket for subscription. " + + "executionResult.getData(): {}", executionResult.getData().toString()); + + return HttpResponse.ofJson(HttpStatus.NOT_IMPLEMENTED, + produceType, + toSpecification( + "Use GraphQL over WebSocket for subscription")); + } + + if (executionResult.getErrors().isEmpty()) { + return HttpResponse.ofJson(produceType, executionResult.toSpecification()); + } + + return errorHandler.handle(ctx, input, executionResult, null); + })); + } catch (Throwable cause) { + cause = Exceptions.peel(cause); + final HttpResponse res = errorHandler.handle(ctx, input, null, cause); + assert res != null : "DefaultGraphqlService.handle() returned null?"; + return res; + } } static Map toSpecification(String message) { diff --git a/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlErrorHandler.java b/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlErrorHandler.java index 02d7afa4c1b..2273db905a8 100644 --- a/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlErrorHandler.java +++ b/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlErrorHandler.java @@ -46,7 +46,8 @@ static GraphqlErrorHandler of() { */ @Nullable HttpResponse handle( - ServiceRequestContext ctx, ExecutionInput input, ExecutionResult result, @Nullable Throwable cause); + ServiceRequestContext ctx, ExecutionInput input, @Nullable ExecutionResult result, + @Nullable Throwable cause); /** * Returns a composed {@link GraphqlErrorHandler} that applies this first and the specified diff --git a/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlWSSubProtocol.java b/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlWSSubProtocol.java index b7c0c2691a2..7188f48891d 100644 --- a/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlWSSubProtocol.java +++ b/graphql/src/main/java/com/linecorp/armeria/server/graphql/GraphqlWSSubProtocol.java @@ -374,6 +374,7 @@ private static void writeError(WebSocketWriter out, String operationId, Throwabl "id", operationId, "payload", ImmutableList.of( new GraphQLError() { + @Nullable @Override public String getMessage() { return t.getMessage(); @@ -430,4 +431,3 @@ public Throwable fillInStackTrace() { } } } - diff --git a/graphql/src/test/java/com/linecorp/armeria/server/graphql/GraphqlErrorHandlerTest.java b/graphql/src/test/java/com/linecorp/armeria/server/graphql/GraphqlErrorHandlerTest.java index 5938af365b5..ac7e7a6732f 100644 --- a/graphql/src/test/java/com/linecorp/armeria/server/graphql/GraphqlErrorHandlerTest.java +++ b/graphql/src/test/java/com/linecorp/armeria/server/graphql/GraphqlErrorHandlerTest.java @@ -20,83 +20,163 @@ import java.io.File; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import com.linecorp.armeria.common.AggregatedHttpResponse; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.common.MediaType; +import com.linecorp.armeria.internal.testing.AnticipatedException; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import graphql.GraphQL; import graphql.GraphQLError; import graphql.GraphqlErrorException; +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; import graphql.schema.DataFetcher; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; class GraphqlErrorHandlerTest { + private static final AtomicBoolean shouldFailRequests = new AtomicBoolean(); + + private static GraphQL newGraphQL() throws Exception { + final File graphqlSchemaFile = + new File(GraphqlErrorHandlerTest.class.getResource("/testing/graphql/test.graphqls").toURI()); + final SchemaParser schemaParser = new SchemaParser(); + final SchemaGenerator schemaGenerator = new SchemaGenerator(); + final TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry(); + typeRegistry.merge(schemaParser.parse(graphqlSchemaFile)); + final RuntimeWiring.Builder runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring(); + final DataFetcher foo = dataFetcher("foo"); + runtimeWiringBuilder.type("Query", + typeWiring -> typeWiring.dataFetcher("foo", foo)); + final DataFetcher error = dataFetcher("error"); + runtimeWiringBuilder.type("Query", + typeWiring -> typeWiring.dataFetcher("error", error)); + + final GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, + runtimeWiringBuilder.build()); + final Instrumentation instrumentation = new Instrumentation() { + @Override + public InstrumentationState createState( + InstrumentationCreateStateParameters parameters) { + if (shouldFailRequests.get()) { + throw new AnticipatedException("external exception"); + } else { + return Instrumentation.super.createState(parameters); + } + } + }; + + return new GraphQL.Builder(graphQLSchema) + .instrumentation(instrumentation) + .build(); + } + + private static final GraphqlErrorHandler errorHandler + = (ctx, input, result, cause) -> { + if (result == null) { + assertThat(cause).isNotNull(); + return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, MediaType.PLAIN_TEXT, + cause.getMessage()); + } + final List errors = result.getErrors(); + if (errors.stream().map(GraphQLError::getMessage).anyMatch(m -> m.endsWith("foo"))) { + return HttpResponse.of(HttpStatus.BAD_REQUEST); + } + return null; + }; + + private static DataFetcher dataFetcher(String value) { + return environment -> { + final ServiceRequestContext ctx = GraphqlServiceContexts.get(environment); + // Make sure that a ServiceRequestContext is available + assertThat(ServiceRequestContext.current()).isSameAs(ctx); + throw GraphqlErrorException.newErrorException().message(value).build(); + }; + } + @RegisterExtension static ServerExtension server = new ServerExtension() { @Override protected void configure(ServerBuilder sb) throws Exception { - final File graphqlSchemaFile = - new File(getClass().getResource("/testing/graphql/test.graphqls").toURI()); - - final GraphqlErrorHandler errorHandler - = (ctx, input, result, cause) -> { - final List errors = result.getErrors(); - if (errors.stream().map(GraphQLError::getMessage).anyMatch(m -> m.endsWith("foo"))) { - return HttpResponse.of(HttpStatus.BAD_REQUEST); - } - return null; - }; final GraphqlService service = GraphqlService.builder() - .schemaFile(graphqlSchemaFile) - .runtimeWiring(c -> { - final DataFetcher foo = dataFetcher("foo"); - c.type("Query", - typeWiring -> typeWiring.dataFetcher("foo", foo)); - final DataFetcher error = dataFetcher("error"); - c.type("Query", - typeWiring -> typeWiring.dataFetcher("error", error)); - }) + .graphql(newGraphQL()) .errorHandler(errorHandler) .build(); sb.service("/graphql", service); } }; - private static DataFetcher dataFetcher(String value) { - return environment -> { - final ServiceRequestContext ctx = GraphqlServiceContexts.get(environment); - assertThat(ctx.eventLoop().inEventLoop()).isTrue(); - // Make sure that a ServiceRequestContext is available - assertThat(ServiceRequestContext.current()).isSameAs(ctx); - throw GraphqlErrorException.newErrorException().message(value).build(); - }; + @RegisterExtension + static ServerExtension blockingServer = new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) throws Exception { + + final GraphqlService service = + GraphqlService.builder() + .graphql(newGraphQL()) + .useBlockingTaskExecutor(true) + .errorHandler(errorHandler) + .build(); + sb.service("/graphql", service); + } + }; + + @BeforeEach + void setUp() { + shouldFailRequests.set(false); } - @Test - void handledError() { + @ValueSource(booleans = { true, false }) + @ParameterizedTest + void handledError(boolean blocking) { final HttpRequest request = HttpRequest.builder().post("/graphql") .content(MediaType.GRAPHQL, "{foo}") .build(); + final ServerExtension server = blocking ? blockingServer : GraphqlErrorHandlerTest.server; final AggregatedHttpResponse response = server.blockingWebClient().execute(request); assertThat(response.status()).isEqualTo(HttpStatus.BAD_REQUEST); } - @Test - void unhandledError() { + @ValueSource(booleans = { true, false }) + @ParameterizedTest + void unhandledGraphqlError(boolean blocking) { final HttpRequest request = HttpRequest.builder().post("/graphql") .content(MediaType.GRAPHQL, "{error}") .build(); + final ServerExtension server = blocking ? blockingServer : GraphqlErrorHandlerTest.server; final AggregatedHttpResponse response = server.blockingWebClient().execute(request); assertThat(response.status()).isEqualTo(HttpStatus.OK); } + + @ValueSource(booleans = { true, false }) + @ParameterizedTest + void unhandledException(boolean blocking) { + shouldFailRequests.set(true); + final HttpRequest request = HttpRequest.builder().post("/graphql") + .content(MediaType.GRAPHQL, "{error}") + .build(); + final ServerExtension server = blocking ? blockingServer : GraphqlErrorHandlerTest.server; + final AggregatedHttpResponse response = server.blockingWebClient().execute(request); + assertThat(response.status()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(response.contentUtf8()).isEqualTo("external exception"); + } } diff --git a/grpc-protocol/src/main/java/com/linecorp/armeria/common/grpc/protocol/DeframedMessage.java b/grpc-protocol/src/main/java/com/linecorp/armeria/common/grpc/protocol/DeframedMessage.java index 2ec4a959d55..1a0b13bce89 100644 --- a/grpc-protocol/src/main/java/com/linecorp/armeria/common/grpc/protocol/DeframedMessage.java +++ b/grpc-protocol/src/main/java/com/linecorp/armeria/common/grpc/protocol/DeframedMessage.java @@ -124,6 +124,7 @@ public void close() { if (buf != null) { buf.release(); } else { + assert stream != null; try { stream.close(); } catch (IOException e) { diff --git a/grpc-protocol/src/main/java/com/linecorp/armeria/server/grpc/protocol/AbstractUnsafeUnaryGrpcService.java b/grpc-protocol/src/main/java/com/linecorp/armeria/server/grpc/protocol/AbstractUnsafeUnaryGrpcService.java index f57380f8983..4aa8515eb14 100644 --- a/grpc-protocol/src/main/java/com/linecorp/armeria/server/grpc/protocol/AbstractUnsafeUnaryGrpcService.java +++ b/grpc-protocol/src/main/java/com/linecorp/armeria/server/grpc/protocol/AbstractUnsafeUnaryGrpcService.java @@ -142,6 +142,7 @@ protected final HttpResponse doPost(ServiceRequestContext ctx, HttpRequest req) final HttpData content = framer.writePayload(responseMessage); final ResponseHeaders responseHeaders = RESPONSE_HEADERS_MAP.get( serializationFormat); + assert responseHeaders != null; if (UnaryGrpcSerializationFormats.isGrpcWeb(serializationFormat)) { // Send trailer as a part of the body for gRPC-web. final HttpData serializedTrailers = framer.writePayload( diff --git a/grpc/src/main/java/com/linecorp/armeria/common/grpc/DefaultGrpcExceptionHandlerFunction.java b/grpc/src/main/java/com/linecorp/armeria/common/grpc/DefaultGrpcExceptionHandlerFunction.java index 4a0203b19e4..53f485b0f55 100644 --- a/grpc/src/main/java/com/linecorp/armeria/common/grpc/DefaultGrpcExceptionHandlerFunction.java +++ b/grpc/src/main/java/com/linecorp/armeria/common/grpc/DefaultGrpcExceptionHandlerFunction.java @@ -49,11 +49,6 @@ public Status apply(RequestContext ctx, Status status, Throwable cause, Metadata if (status.getCode() != Code.UNKNOWN) { return status; } - final Status s = Status.fromThrowable(cause); - if (s.getCode() != Code.UNKNOWN) { - return s; - } - if (cause instanceof ClosedSessionException || cause instanceof ClosedChannelException) { if (ctx instanceof ServiceRequestContext) { // Upstream uses CANCELLED @@ -63,7 +58,7 @@ public Status apply(RequestContext ctx, Status status, Throwable cause, Metadata // ClosedChannelException is used any time the Netty channel is closed. Proper error // processing requires remembering the error that occurred before this one and using it // instead. - return s; + return status; } if (cause instanceof ClosedStreamException || cause instanceof RequestTimeoutException) { return Status.CANCELLED.withCause(cause); @@ -89,6 +84,6 @@ public Status apply(RequestContext ctx, Status status, Throwable cause, Metadata if (cause instanceof ContentTooLargeException) { return Status.RESOURCE_EXHAUSTED.withCause(cause); } - return s; + return status; } } diff --git a/grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcExceptionHandlerFunction.java b/grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcExceptionHandlerFunction.java index 6e9f150e7d0..db7802033ad 100644 --- a/grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcExceptionHandlerFunction.java +++ b/grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcExceptionHandlerFunction.java @@ -65,6 +65,9 @@ static GrpcExceptionHandlerFunction of() { */ default GrpcExceptionHandlerFunction orElse(GrpcExceptionHandlerFunction next) { requireNonNull(next, "next"); + if (this == next) { + return this; + } return (ctx, status, cause, metadata) -> { final Status newStatus = apply(ctx, status, cause, metadata); if (newStatus != null) { diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaChannel.java b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaChannel.java index c35beeb48cc..a75dea16adf 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaChannel.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaChannel.java @@ -44,13 +44,13 @@ import com.linecorp.armeria.common.SessionProtocol; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.grpc.GrpcCallOptions; -import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.GrpcJsonMarshaller; import com.linecorp.armeria.common.logging.RequestLogProperty; import com.linecorp.armeria.common.util.SystemInfo; import com.linecorp.armeria.common.util.Unwrappable; import com.linecorp.armeria.internal.client.DefaultClientRequestContext; import com.linecorp.armeria.internal.common.RequestTargetCache; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import io.grpc.CallCredentials; import io.grpc.CallOptions; @@ -98,7 +98,7 @@ final class ArmeriaChannel extends Channel implements ClientBuilderParams, Unwra private final Compressor compressor; private final DecompressorRegistry decompressorRegistry; private final CallCredentials credentials0; - private final GrpcExceptionHandlerFunction exceptionHandler; + private final InternalGrpcExceptionHandler exceptionHandler; private final boolean useMethodMarshaller; ArmeriaChannel(ClientBuilderParams params, @@ -124,7 +124,7 @@ final class ArmeriaChannel extends Channel implements ClientBuilderParams, Unwra compressor = options.get(GrpcClientOptions.COMPRESSOR); decompressorRegistry = options.get(GrpcClientOptions.DECOMPRESSOR_REGISTRY); credentials0 = options.get(GrpcClientOptions.CALL_CREDENTIALS); - exceptionHandler = options.get(GrpcClientOptions.EXCEPTION_HANDLER); + exceptionHandler = new InternalGrpcExceptionHandler(options.get(GrpcClientOptions.EXCEPTION_HANDLER)); } @Override @@ -221,6 +221,7 @@ public ClientOptions options() { return params.options(); } + @Nullable @Override public T as(Class type) { final T unwrapped = Unwrappable.super.as(type); @@ -238,6 +239,9 @@ private DefaultClientRequestContext newContext(HttpMethod method, HttpReq assert reqTarget != null : path; RequestTargetCache.putForClient(path, reqTarget); + final RequestOptions requestOptions = REQUEST_OPTIONS_MAP.get(methodDescriptor.getType()); + assert requestOptions != null; + return new DefaultClientRequestContext( meterRegistry, sessionProtocol, @@ -247,7 +251,7 @@ private DefaultClientRequestContext newContext(HttpMethod method, HttpReq options(), req, null, - REQUEST_OPTIONS_MAP.get(methodDescriptor.getType()), + requestOptions, System.nanoTime(), SystemInfo.currentTimeMicros()); } diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java index a395d37fce7..e996aa012b8 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java @@ -17,8 +17,6 @@ import static com.linecorp.armeria.internal.client.ClientUtil.initContextAndExecuteWithFallback; import static com.linecorp.armeria.internal.client.grpc.protocol.InternalGrpcWebUtil.messageBuf; -import static com.linecorp.armeria.internal.common.grpc.GrpcExceptionHandlerFunctionUtil.fromThrowable; -import static com.linecorp.armeria.internal.common.grpc.GrpcExceptionHandlerFunctionUtil.generateMetadataFromThrowable; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -50,7 +48,6 @@ import com.linecorp.armeria.common.RequestHeadersBuilder; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.annotation.Nullable; -import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.GrpcJsonMarshaller; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.grpc.protocol.ArmeriaMessageFramer; @@ -71,6 +68,7 @@ import com.linecorp.armeria.internal.common.grpc.GrpcMessageMarshaller; import com.linecorp.armeria.internal.common.grpc.GrpcStatus; import com.linecorp.armeria.internal.common.grpc.HttpStreamDeframer; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import com.linecorp.armeria.internal.common.grpc.MetadataUtil; import com.linecorp.armeria.internal.common.grpc.StatusAndMetadata; import com.linecorp.armeria.internal.common.grpc.TimeoutHeaderUtil; @@ -127,7 +125,7 @@ final class ArmeriaClientCall extends ClientCall private final int maxInboundMessageSizeBytes; private final boolean grpcWebText; private final Compressor compressor; - private final GrpcExceptionHandlerFunction exceptionHandler; + private final InternalGrpcExceptionHandler exceptionHandler; private boolean endpointInitialized; @Nullable @@ -162,7 +160,7 @@ final class ArmeriaClientCall extends ClientCall SerializationFormat serializationFormat, @Nullable GrpcJsonMarshaller jsonMarshaller, boolean unsafeWrapResponseBuffers, - GrpcExceptionHandlerFunction exceptionHandler, + InternalGrpcExceptionHandler exceptionHandler, boolean useMethodMarshaller) { this.ctx = ctx; this.endpointGroup = endpointGroup; @@ -251,8 +249,8 @@ public void start(Listener responseListener, Metadata metadata) { final BiFunction errorResponseFactory = (unused, cause) -> { - final Metadata responseMetadata = generateMetadataFromThrowable(cause); - Status status = fromThrowable(ctx, exceptionHandler, cause, responseMetadata); + final StatusAndMetadata statusAndMetadata = exceptionHandler.handle(ctx, cause); + Status status = statusAndMetadata.status(); if (status.getDescription() == null) { status = status.withDescription(cause.getMessage()); } @@ -460,8 +458,8 @@ public void onNext(DeframedMessage message) { } }); } catch (Throwable t) { - final Metadata metadata = generateMetadataFromThrowable(t); - close(fromThrowable(ctx, exceptionHandler, t, metadata), metadata); + final StatusAndMetadata statusAndMetadata = exceptionHandler.handle(ctx, t); + close(statusAndMetadata.status(), statusAndMetadata.metadata()); } } @@ -476,7 +474,7 @@ public void onComplete() { } @Override - public void transportReportStatus(Status status, Metadata metadata) { + public void transportReportStatus(Status status, @Nullable Metadata metadata) { // This method is invoked in ctx.eventLoop and we need to bounce it through // CallOptions executor (in case it's configured) to serialize with closes // that were potentially triggered when Listener throws (closeWhenListenerThrows). @@ -517,11 +515,11 @@ private void prepareHeaders(Compressor compressor, Metadata metadata, long remai } private void closeWhenListenerThrows(Throwable t) { - final Metadata metadata = generateMetadataFromThrowable(t); - closeWhenEos(fromThrowable(ctx, exceptionHandler, t, metadata), metadata); + final StatusAndMetadata statusAndMetadata = exceptionHandler.handle(ctx, t); + closeWhenEos(statusAndMetadata.status(), statusAndMetadata.metadata()); } - private void closeWhenEos(Status status, Metadata metadata) { + private void closeWhenEos(Status status, @Nullable Metadata metadata) { if (needsDirectInvocation()) { close(status, metadata); } else { @@ -537,7 +535,7 @@ private void closeWhenEos(Status status, Metadata metadata) { // never do this concurrently: the abnormal call into close() from the caller thread happens in case // of early return, before event-loop is being assigned to this call. After event-loop is being // assigned, the driving call won't be able to trigger close() anymore. - private void close(Status status, Metadata metadata) { + private void close(Status status, @Nullable Metadata metadata) { if (closed) { // 'close()' could be called twice if a call is closed with non-OK status. // See: https://github.com/line/armeria/issues/3799 @@ -550,7 +548,10 @@ private void close(Status status, Metadata metadata) { status = Status.DEADLINE_EXCEEDED; // Replace trailers to prevent mixing sources of status and trailers. metadata = new Metadata(); + } else if (metadata == null) { + metadata = new Metadata(); } + if (status.getCode() == Code.DEADLINE_EXCEEDED) { status = status.augmentDescription("deadline exceeded after " + MILLISECONDS.toNanos(ctx.responseTimeoutMillis()) + "ns."); diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/GrpcClientFactory.java b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/GrpcClientFactory.java index 1970256c4d0..1e04f8db90d 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/GrpcClientFactory.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/GrpcClientFactory.java @@ -47,9 +47,12 @@ import com.linecorp.armeria.client.grpc.GrpcClientStubFactory; import com.linecorp.armeria.client.grpc.protocol.UnaryGrpcClient; import com.linecorp.armeria.client.retry.RetryingClient; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.common.Scheme; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.grpc.GrpcJsonMarshaller; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.util.Unwrappable; @@ -199,7 +202,7 @@ private static ClientBuilderParams addTrailersExtractor( originalDecoration.decorators(); boolean foundRetryingClient = false; - final HttpClient noopClient = (ctx, req) -> null; + final HttpClient noopClient = (ctx, req) -> HttpResponse.of(HttpStatus.OK); for (Function decorator : decorators) { final HttpClient decorated = decorator.apply(noopClient); if (decorated instanceof RetryingClient) { @@ -225,6 +228,7 @@ private static ClientBuilderParams addTrailersExtractor( params.clientType(), optionsBuilder.build()); } + @Nullable @Override public T unwrap(Object client, Class type) { final T unwrapped = super.unwrap(client, type); diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/JavaGrpcClientStubFactory.java b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/JavaGrpcClientStubFactory.java index 5a01344104b..998a7ae681d 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/JavaGrpcClientStubFactory.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/JavaGrpcClientStubFactory.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import com.linecorp.armeria.client.grpc.GrpcClientStubFactory; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.Exceptions; import io.grpc.Channel; @@ -32,6 +33,7 @@ */ public final class JavaGrpcClientStubFactory implements GrpcClientStubFactory { + @Nullable @Override public ServiceDescriptor findServiceDescriptor(Class clientType) { final String clientTypeName = clientType.getName(); @@ -59,6 +61,7 @@ public ServiceDescriptor findServiceDescriptor(Class clientType) { @Override public Object newClientStub(Class clientType, Channel channel) { final Method stubFactoryMethod = GrpcClientFactoryUtil.findStubFactoryMethod(clientType); + assert stubFactoryMethod != null : "No stub factory method found for " + clientType; try { return stubFactoryMethod.invoke(null, channel); } catch (IllegalAccessException | InvocationTargetException e) { diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/NullGrpcClientStubFactory.java b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/NullGrpcClientStubFactory.java index e0cfde8d555..031a28ac1ef 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/NullGrpcClientStubFactory.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/NullGrpcClientStubFactory.java @@ -17,7 +17,6 @@ package com.linecorp.armeria.internal.client.grpc; import com.linecorp.armeria.client.grpc.GrpcClientStubFactory; -import com.linecorp.armeria.common.annotation.Nullable; import io.grpc.Channel; import io.grpc.ServiceDescriptor; @@ -31,7 +30,6 @@ public ServiceDescriptor findServiceDescriptor(Class clientType) { throw new UnsupportedOperationException(); } - @Nullable @Override public Object newClientStub(Class clientType, Channel channel) { throw new UnsupportedOperationException(); diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ReactorGrpcClientStubFactory.java b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ReactorGrpcClientStubFactory.java index 682beee88f3..e9852df8362 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ReactorGrpcClientStubFactory.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ReactorGrpcClientStubFactory.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import com.linecorp.armeria.client.grpc.GrpcClientStubFactory; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.Exceptions; import io.grpc.Channel; @@ -32,6 +33,7 @@ */ public final class ReactorGrpcClientStubFactory implements GrpcClientStubFactory { + @Nullable @Override public ServiceDescriptor findServiceDescriptor(Class clientType) { final String clientTypeName = clientType.getName(); @@ -53,6 +55,7 @@ public ServiceDescriptor findServiceDescriptor(Class clientType) { @Override public Object newClientStub(Class clientType, Channel channel) { final Method stubFactoryMethod = GrpcClientFactoryUtil.findStubFactoryMethod(clientType); + assert stubFactoryMethod != null : "No stub factory method found for " + clientType; try { return stubFactoryMethod.invoke(null, channel); } catch (IllegalAccessException | InvocationTargetException e) { diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ScalaPbGrpcClientStubFactory.java b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ScalaPbGrpcClientStubFactory.java index d84b2ad1647..8e9657c25e1 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ScalaPbGrpcClientStubFactory.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ScalaPbGrpcClientStubFactory.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import com.linecorp.armeria.client.grpc.GrpcClientStubFactory; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.Exceptions; import io.grpc.Channel; @@ -32,6 +33,7 @@ */ public final class ScalaPbGrpcClientStubFactory implements GrpcClientStubFactory { + @Nullable @Override public ServiceDescriptor findServiceDescriptor(Class clientType) { final Class stubClass = clientType.getEnclosingClass(); @@ -50,6 +52,7 @@ public ServiceDescriptor findServiceDescriptor(Class clientType) { @Override public Object newClientStub(Class clientType, Channel channel) { final Method stubFactoryMethod = GrpcClientFactoryUtil.findStubFactoryMethod(clientType); + assert stubFactoryMethod != null : "No stub factory method found for " + clientType; try { return stubFactoryMethod.invoke(null, channel); } catch (IllegalAccessException | InvocationTargetException e) { diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ServiceDescriptorResolutionException.java b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ServiceDescriptorResolutionException.java index 23d1e6864c5..eaff46d76eb 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ServiceDescriptorResolutionException.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ServiceDescriptorResolutionException.java @@ -36,6 +36,6 @@ public ServiceDescriptorResolutionException(String stubFactoryName, Throwable ca @Override public String toString() { - return stubFactoryName + '=' + getCause().toString(); + return stubFactoryName + '=' + getCause(); } } diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/GrpcExceptionHandlerFunctionUtil.java b/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/GrpcExceptionHandlerFunctionUtil.java deleted file mode 100644 index 5a87733d532..00000000000 --- a/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/GrpcExceptionHandlerFunctionUtil.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2024 LINE Corporation - * - * LINE Corporation licenses this file to you 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: - * - * https://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.linecorp.armeria.internal.common.grpc; - -import static java.util.Objects.requireNonNull; - -import com.linecorp.armeria.common.RequestContext; -import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; -import com.linecorp.armeria.common.grpc.protocol.ArmeriaStatusException; -import com.linecorp.armeria.common.util.Exceptions; - -import io.grpc.Metadata; -import io.grpc.Status; - -public final class GrpcExceptionHandlerFunctionUtil { - - public static Metadata generateMetadataFromThrowable(Throwable exception) { - final Metadata metadata = Status.trailersFromThrowable(peelAndUnwrap(exception)); - return metadata != null ? metadata : new Metadata(); - } - - public static Status fromThrowable(RequestContext ctx, GrpcExceptionHandlerFunction exceptionHandler, - Throwable t, Metadata metadata) { - final Status status = Status.fromThrowable(peelAndUnwrap(t)); - final Throwable cause = status.getCause(); - if (cause == null) { - return status; - } - return applyExceptionHandler(ctx, exceptionHandler, status, cause, metadata); - } - - public static Status applyExceptionHandler(RequestContext ctx, - GrpcExceptionHandlerFunction exceptionHandler, - Status status, Throwable cause, Metadata metadata) { - final Throwable peeled = peelAndUnwrap(cause); - status = exceptionHandler.apply(ctx, status, peeled, metadata); - assert status != null; - return status; - } - - private static Throwable peelAndUnwrap(Throwable t) { - requireNonNull(t, "t"); - t = Exceptions.peel(t); - Throwable cause = t; - while (cause != null) { - if (cause instanceof ArmeriaStatusException) { - return StatusExceptionConverter.toGrpc((ArmeriaStatusException) cause); - } - cause = cause.getCause(); - } - return t; - } - - private GrpcExceptionHandlerFunctionUtil() {} -} diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/GrpcMessageMarshaller.java b/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/GrpcMessageMarshaller.java index 3b5a23778cf..6a5f73ea09c 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/GrpcMessageMarshaller.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/GrpcMessageMarshaller.java @@ -103,6 +103,7 @@ public ByteBuf serializeRequest(I message) throws IOException { ByteStreams.copy(is, os); } } else { + assert jsonMarshaller != null; jsonMarshaller.serializeMessage(requestMarshaller, message, os); } } @@ -133,10 +134,13 @@ public I deserializeRequest(DeframedMessage message, boolean grpcWebText) throws } } } + + assert messageStream != null; try (InputStream msg = messageStream) { if (isProto) { return method.parseRequest(msg); } else { + assert jsonMarshaller != null; return jsonMarshaller.deserializeMessage(requestMarshaller, msg); } } @@ -157,6 +161,7 @@ public ByteBuf serializeResponse(O message) throws IOException { ByteStreams.copy(is, os); } } else { + assert jsonMarshaller != null; jsonMarshaller.serializeMessage(responseMarshaller, message, os); } } @@ -188,10 +193,13 @@ public O deserializeResponse(DeframedMessage message, boolean grpcWebText) throw } } } + + assert messageStream != null; try (InputStream msg = messageStream) { if (isProto) { return method.parseResponse(msg); } else { + assert jsonMarshaller != null; return jsonMarshaller.deserializeMessage(responseMarshaller, msg); } } @@ -231,6 +239,7 @@ private ByteBuf serializeProto(PrototypeMarshaller marshaller, Message me try (ByteBufOutputStream os = new ByteBufOutputStream(buf)) { @SuppressWarnings("unchecked") final T cast = (T) message; + assert jsonMarshaller != null; jsonMarshaller.serializeMessage(marshaller, cast, os); success = true; } finally { @@ -275,6 +284,7 @@ private Message deserializeProto(PrototypeMarshaller marshaller, ByteBuf } } else { try (ByteBufInputStream is = new ByteBufInputStream(buf, /* releaseOnClose */ false)) { + assert jsonMarshaller != null; return (Message) jsonMarshaller.deserializeMessage(marshaller, is); } } diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/HttpStreamDeframer.java b/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/HttpStreamDeframer.java index 4fb84f701c6..9c52ccf4c71 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/HttpStreamDeframer.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/common/grpc/HttpStreamDeframer.java @@ -17,8 +17,6 @@ package com.linecorp.armeria.internal.common.grpc; import static com.google.common.base.Preconditions.checkState; -import static com.linecorp.armeria.internal.common.grpc.GrpcExceptionHandlerFunctionUtil.fromThrowable; -import static com.linecorp.armeria.internal.common.grpc.GrpcExceptionHandlerFunctionUtil.generateMetadataFromThrowable; import static java.util.Objects.requireNonNull; import com.linecorp.armeria.common.HttpHeaderNames; @@ -28,7 +26,6 @@ import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.RequestHeaders; import com.linecorp.armeria.common.annotation.Nullable; -import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.protocol.ArmeriaMessageDeframer; import com.linecorp.armeria.common.grpc.protocol.Decompressor; import com.linecorp.armeria.common.grpc.protocol.DeframedMessage; @@ -46,7 +43,7 @@ public final class HttpStreamDeframer extends ArmeriaMessageDeframer { private final RequestContext ctx; private final DecompressorRegistry decompressorRegistry; private final TransportStatusListener transportStatusListener; - private final GrpcExceptionHandlerFunction exceptionHandler; + private final InternalGrpcExceptionHandler exceptionHandler; @Nullable private StreamMessage deframedStreamMessage; @@ -57,7 +54,7 @@ public HttpStreamDeframer( DecompressorRegistry decompressorRegistry, RequestContext ctx, TransportStatusListener transportStatusListener, - GrpcExceptionHandlerFunction exceptionHandler, + InternalGrpcExceptionHandler exceptionHandler, int maxMessageLength, boolean grpcWebText, boolean server) { super(maxMessageLength, ctx.alloc(), grpcWebText); this.ctx = requireNonNull(ctx, "ctx"); @@ -121,9 +118,9 @@ public void processHeaders(HttpHeaders headers, StreamDecoderOutput extends ServerCall { @Nullable private final Executor blockingExecutor; - private final GrpcExceptionHandlerFunction exceptionHandler; + private final InternalGrpcExceptionHandler exceptionHandler; // Only set once. @Nullable @@ -151,7 +148,7 @@ protected AbstractServerCall(HttpRequest req, @Nullable GrpcJsonMarshaller jsonMarshaller, boolean unsafeWrapRequestBuffers, ResponseHeaders defaultHeaders, - GrpcExceptionHandlerFunction exceptionHandler, + InternalGrpcExceptionHandler exceptionHandler, @Nullable Executor blockingExecutor, boolean autoCompression, boolean useMethodMarshaller) { @@ -204,41 +201,34 @@ protected final void maybeCancel() { if (!closeCalled) { cancelled = true; try (SafeCloseable ignore = ctx.push()) { - close(new ServerStatusAndMetadata(Status.CANCELLED, new Metadata(), true, true)); + close(new ServerStatusAndMetadata(Status.CANCELLED, new Metadata(), true)); } } } - public final void close(Throwable exception) { - close(exception, false); - } - - public final void close(Throwable exception, boolean cancelled) { - exception = Exceptions.peel(exception); - final Metadata metadata = generateMetadataFromThrowable(exception); - final Status status = Status.fromThrowable(exception); - close(status, metadata, cancelled, exception); - } - @Override public final void close(Status status, Metadata metadata) { - close(status, metadata, false, null); - } - - private void close(Status status, Metadata metadata, boolean cancelled, - @Nullable Throwable originalCause) { final Throwable cause = status.getCause(); if (cause == null) { - close(new ServerStatusAndMetadata(status, metadata, false, cancelled)); + close(new ServerStatusAndMetadata(status, metadata)); return; } - Status newStatus = applyExceptionHandler(ctx, exceptionHandler, status, cause, metadata); + + Status newStatus = exceptionHandler.handle(ctx, status, cause, metadata); if (status.getDescription() != null) { newStatus = newStatus.withDescription(status.getDescription()); } - final ServerStatusAndMetadata statusAndMetadata = - new ServerStatusAndMetadata(newStatus, metadata, false, cancelled); - close(statusAndMetadata, firstNonNull(originalCause, cause)); + close(new ServerStatusAndMetadata(newStatus, metadata), cause); + } + + public final void close(Throwable exception) { + close(exception, false); + } + + protected final void close(Throwable exception, boolean cancelled) { + final StatusAndMetadata statusAndMetadata = exceptionHandler.handle(ctx, exception); + close(new ServerStatusAndMetadata(statusAndMetadata.status(), statusAndMetadata.metadata(), + cancelled), exception); } public final void close(ServerStatusAndMetadata statusAndMetadata) { @@ -261,15 +251,13 @@ private void doClose(ServerStatusAndMetadata statusAndMetadata, @Nullable Throwa final Metadata metadata = statusAndMetadata.metadata(); if (isCancelled()) { // No need to write anything to client if cancelled already. - statusAndMetadata.shouldCancel(); - statusAndMetadata.setResponseContent(true); + statusAndMetadata.shouldCancel(true); closeListener(statusAndMetadata); return; } if (status.getCode() == Code.CANCELLED && status.getCause() instanceof ClosedStreamException) { - statusAndMetadata.shouldCancel(); - statusAndMetadata.setResponseContent(true); + statusAndMetadata.shouldCancel(true); closeListener(statusAndMetadata); return; } @@ -284,7 +272,7 @@ private void doClose(ServerStatusAndMetadata statusAndMetadata, @Nullable Throwa logger.warn("{} {} status: {}, metadata: {}", ctx, description, status, metadata); status = Status.CANCELLED.withDescription(description); statusAndMetadata = statusAndMetadata.withStatus(status); - statusAndMetadata.shouldCancel(); + statusAndMetadata.shouldCancel(true); } doClose(statusAndMetadata); } @@ -292,8 +280,7 @@ private void doClose(ServerStatusAndMetadata statusAndMetadata, @Nullable Throwa protected abstract void doClose(ServerStatusAndMetadata statusAndMetadata); protected final void closeListener(ServerStatusAndMetadata statusAndMetadata) { - final boolean setResponseContent = statusAndMetadata.setResponseContent(); - final boolean cancelled = statusAndMetadata.isShouldCancel(); + final boolean cancelled = statusAndMetadata.shouldCancel(); if (!listenerClosed) { listenerClosed = true; @@ -302,7 +289,7 @@ protected final void closeListener(ServerStatusAndMetadata statusAndMetadata) { ctx.logBuilder().requestContent(GrpcLogUtil.rpcRequest(method, simpleMethodName), null); } - if (setResponseContent) { + if (!ctx.log().isAvailable(RequestLogProperty.RESPONSE_CONTENT)) { ctx.logBuilder().responseContent(GrpcLogUtil.rpcResponse(statusAndMetadata, firstResponse()), null); } @@ -349,7 +336,7 @@ public void onRequestMessage(DeframedMessage message, boolean endOfStream) { final Status status = Status.INTERNAL.withDescription( "More than one request messages for unary call or server streaming " + "call"); - closeListener(new ServerStatusAndMetadata(status, new Metadata(), true, true)); + closeListener(new ServerStatusAndMetadata(status, new Metadata(), true)); return; } messageReceived = true; @@ -522,6 +509,7 @@ private void doSendHeaders(Metadata metadata) { if (compressor != Codec.Identity.NONE || InternalMetadata.headerCount(metadata) > 0) { headers = headers.withMutations(builder -> { if (compressor != Codec.Identity.NONE) { + assert compressor != null; builder.set(GrpcHeaderNames.GRPC_ENCODING, compressor.getMessageEncoding()); } MetadataUtil.fillHeaders(metadata, builder); @@ -548,7 +536,7 @@ protected final HttpData toPayload(O message) throws IOException { } protected final HttpObject responseTrailers(ServiceRequestContext ctx, Status status, - Metadata metadata, boolean trailersOnly) { + @Nullable Metadata metadata, boolean trailersOnly) { final HttpHeadersBuilder defaultTrailers = trailersOnly ? defaultResponseHeaders.toBuilder() : HttpHeaders.builder(); final HttpHeaders trailers = statusToTrailers(ctx, defaultTrailers, status, metadata); @@ -564,7 +552,8 @@ protected final HttpObject responseTrailers(ServiceRequestContext ctx, Status st // Returns ResponseHeaders if headersSent == false or HttpHeaders otherwise. public static HttpHeaders statusToTrailers( - ServiceRequestContext ctx, HttpHeadersBuilder trailersBuilder, Status status, Metadata metadata) { + ServiceRequestContext ctx, HttpHeadersBuilder trailersBuilder, + Status status, @Nullable Metadata metadata) { try { MetadataUtil.fillHeaders(metadata, trailersBuilder); } catch (Exception e) { @@ -670,7 +659,7 @@ public final ServiceRequestContext ctx() { return ctx; } - public final GrpcExceptionHandlerFunction exceptionHandler() { + public final InternalGrpcExceptionHandler exceptionHandler() { return exceptionHandler; } } diff --git a/grpc/src/main/java/com/linecorp/armeria/internal/server/grpc/ServerStatusAndMetadata.java b/grpc/src/main/java/com/linecorp/armeria/internal/server/grpc/ServerStatusAndMetadata.java index e917adeee90..52d218e1720 100644 --- a/grpc/src/main/java/com/linecorp/armeria/internal/server/grpc/ServerStatusAndMetadata.java +++ b/grpc/src/main/java/com/linecorp/armeria/internal/server/grpc/ServerStatusAndMetadata.java @@ -27,43 +27,26 @@ public final class ServerStatusAndMetadata extends StatusAndMetadata { private boolean shouldCancel; - // Set true if response content log should be written - private boolean setResponseContent; - public ServerStatusAndMetadata(Status status, @Nullable Metadata metadata, boolean setResponseContent) { + public ServerStatusAndMetadata(Status status, @Nullable Metadata metadata) { super(status, metadata); - this.setResponseContent = setResponseContent; } - public ServerStatusAndMetadata(Status status, @Nullable Metadata metadata, boolean setResponseContent, - boolean shouldCancel) { + public ServerStatusAndMetadata(Status status, @Nullable Metadata metadata, boolean shouldCancel) { super(status, metadata); - this.setResponseContent = setResponseContent; this.shouldCancel = shouldCancel; } - public boolean isShouldCancel() { + public boolean shouldCancel() { return shouldCancel; } - /** - * Tries to mark whether the call should be cancelled. If a call path - * has already set the status to be cancelled, subsequent calls have no effect. - */ - public void shouldCancel() { - shouldCancel = true; - } - - public void setResponseContent(boolean setResponseContent) { - this.setResponseContent = setResponseContent; - } - - public boolean setResponseContent() { - return setResponseContent; + public void shouldCancel(boolean cancel) { + shouldCancel = cancel; } public ServerStatusAndMetadata withStatus(Status status) { - return new ServerStatusAndMetadata(status, metadata(), setResponseContent(), isShouldCancel()); + return new ServerStatusAndMetadata(status, metadata(), shouldCancel()); } @Override @@ -72,7 +55,6 @@ public String toString() { .add("status", status()) .add("metadata", metadata()) .add("shouldCancel", shouldCancel) - .add("setResponseContent", setResponseContent) .toString(); } } diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/AbstractUnframedGrpcService.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/AbstractUnframedGrpcService.java index 4970e7f658c..6d6640642cc 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/AbstractUnframedGrpcService.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/AbstractUnframedGrpcService.java @@ -63,6 +63,7 @@ import io.grpc.ServerServiceDefinition; import io.grpc.Status; import io.grpc.Status.Code; +import io.netty.buffer.ByteBuf; import io.netty.util.AttributeKey; /** @@ -142,8 +143,7 @@ protected void frameAndServe( RequestHeaders grpcHeaders, HttpData content, CompletableFuture res, - @Nullable Function responseBodyConverter, - MediaType responseContentType) { + @Nullable Function responseConverter) { final HttpRequest grpcRequest; ctx.setAttr(IS_UNFRAMED_GRPC, true); try (ArmeriaMessageFramer framer = new ArmeriaMessageFramer( @@ -177,7 +177,7 @@ protected void frameAndServe( res.completeExceptionally(t); } else { deframeAndRespond(ctx, framedResponse, res, unframedGrpcErrorHandler, - responseBodyConverter, responseContentType); + responseConverter); } } return null; @@ -189,8 +189,8 @@ static void deframeAndRespond(ServiceRequestContext ctx, AggregatedHttpResponse grpcResponse, CompletableFuture res, UnframedGrpcErrorHandler unframedGrpcErrorHandler, - @Nullable Function responseBodyConverter, - MediaType responseContentType) { + @Nullable + Function responseConverter) { final HttpHeaders trailers = !grpcResponse.trailers().isEmpty() ? grpcResponse.trailers() : grpcResponse.headers(); final String grpcStatusCode = trailers.get(GrpcHeaderNames.GRPC_STATUS); @@ -226,19 +226,19 @@ static void deframeAndRespond(ServiceRequestContext ctx, final ResponseHeadersBuilder unframedHeaders = grpcResponse.headers().toBuilder(); unframedHeaders.set(GrpcHeaderNames.GRPC_STATUS, grpcStatusCode); // grpcStatusCode is 0 which is OK. - unframedHeaders.contentType(responseContentType); final ArmeriaMessageDeframer deframer = new ArmeriaMessageDeframer( // Max outbound message size is handled by the GrpcService, so we don't need to set it here. Integer.MAX_VALUE); + final Subscriber subscriber = singleSubscriber( + unframedHeaders, res, responseConverter); grpcResponse.toHttpResponse().decode(deframer, ctx.alloc()) - .subscribe(singleSubscriber(unframedHeaders, res, responseBodyConverter), ctx.eventLoop(), - SubscriptionOption.WITH_POOLED_OBJECTS); + .subscribe(subscriber, ctx.eventLoop(), SubscriptionOption.WITH_POOLED_OBJECTS); } static Subscriber singleSubscriber( ResponseHeadersBuilder unframedHeaders, CompletableFuture res, - @Nullable Function responseBodyConverter) { + @Nullable Function responseConverter) { return new Subscriber() { @Override @@ -249,12 +249,21 @@ public void onSubscribe(Subscription subscription) { @Override public void onNext(DeframedMessage message) { // We know that we don't support compression, so this is always a ByteBuf. - HttpData unframedContent = HttpData.wrap(message.buf()); - if (responseBodyConverter != null) { - unframedContent = responseBodyConverter.apply(unframedContent); + final ByteBuf buf = message.buf(); + assert buf != null; + final HttpData unframedContent = HttpData.wrap(buf); + unframedHeaders.contentType(MediaType.JSON_UTF_8); + + final AggregatedHttpResponse existingResponse = AggregatedHttpResponse.of( + unframedHeaders.build(), + unframedContent); + + if (responseConverter != null) { + final AggregatedHttpResponse convertedResponse = responseConverter.apply(existingResponse); + res.complete(convertedResponse.toHttpResponse()); + } else { + res.complete(existingResponse.toHttpResponse()); } - unframedHeaders.contentLength(unframedContent.length()); - res.complete(HttpResponse.of(unframedHeaders.build(), unframedContent)); } @Override diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/DeferredListener.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/DeferredListener.java index 3f24a173430..a61aaa7bfa2 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/DeferredListener.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/DeferredListener.java @@ -69,6 +69,7 @@ final class DeferredListener extends ServerCall.Listener { } this.delegate = delegate; + assert pendingQueue != null; try { for (;;) { final Consumer> task = pendingQueue.poll(); @@ -142,6 +143,7 @@ private void maybeAddPendingTask(Consumer> task) { } private void addPendingTask(Consumer> task) { + assert pendingQueue != null; pendingQueue.add(task); } diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/FramedGrpcService.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/FramedGrpcService.java index 0ad408da047..9e6504eecd2 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/FramedGrpcService.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/FramedGrpcService.java @@ -18,9 +18,6 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static com.linecorp.armeria.internal.common.grpc.GrpcExceptionHandlerFunctionUtil.applyExceptionHandler; -import static com.linecorp.armeria.internal.common.grpc.GrpcExceptionHandlerFunctionUtil.fromThrowable; -import static com.linecorp.armeria.internal.common.grpc.GrpcExceptionHandlerFunctionUtil.generateMetadataFromThrowable; import static com.linecorp.armeria.internal.common.grpc.GrpcExchangeTypeUtil.toExchangeType; import static java.util.Objects.requireNonNull; @@ -55,7 +52,6 @@ import com.linecorp.armeria.common.ResponseHeadersBuilder; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.annotation.Nullable; -import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.GrpcJsonMarshaller; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.grpc.protocol.ArmeriaMessageDeframer; @@ -63,7 +59,9 @@ import com.linecorp.armeria.common.logging.RequestLogProperty; import com.linecorp.armeria.common.util.SafeCloseable; import com.linecorp.armeria.common.util.TimeoutMode; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import com.linecorp.armeria.internal.common.grpc.MetadataUtil; +import com.linecorp.armeria.internal.common.grpc.StatusAndMetadata; import com.linecorp.armeria.internal.common.grpc.TimeoutHeaderUtil; import com.linecorp.armeria.internal.server.grpc.AbstractServerCall; import com.linecorp.armeria.internal.server.grpc.ServerStatusAndMetadata; @@ -217,10 +215,12 @@ protected HttpResponse doPost(ServiceRequestContext ctx, HttpRequest req) throws final ServerMethodDefinition method = methodDefinition(ctx); if (method == null) { + final ResponseHeaders defaultHeaders = this.defaultHeaders.get(serializationFormat); + assert defaultHeaders != null; return HttpResponse.of( (ResponseHeaders) AbstractServerCall.statusToTrailers( ctx, - defaultHeaders.get(serializationFormat).toBuilder(), + defaultHeaders.toBuilder(), Status.UNIMPLEMENTED.withDescription( "Method not found: " + ctx.config().route().patternString()), new Metadata())); @@ -238,13 +238,15 @@ protected HttpResponse doPost(ServiceRequestContext ctx, HttpRequest req) throws } } catch (IllegalArgumentException e) { final Metadata metadata = new Metadata(); - final GrpcExceptionHandlerFunction exceptionHandler = registry.getExceptionHandler(method); + final InternalGrpcExceptionHandler exceptionHandler = registry.getExceptionHandler(method); assert exceptionHandler != null; final Status status = Status.INVALID_ARGUMENT.withCause(e); + final ResponseHeaders defaultHeaders = this.defaultHeaders.get(serializationFormat); + assert defaultHeaders != null; return HttpResponse.of( (ResponseHeaders) AbstractServerCall.statusToTrailers( - ctx, defaultHeaders.get(serializationFormat).toBuilder(), - applyExceptionHandler(ctx, exceptionHandler, status, e, metadata), + ctx, defaultHeaders.toBuilder(), + exceptionHandler.handle(ctx, status, e, metadata), metadata)); } } else { @@ -326,10 +328,9 @@ private static void startCall(ServerMethodDefinition methodDef, Ser call.setListener(listener); call.startDeframing(); ctx.whenRequestCancelling().handle((cancellationCause, unused) -> { - final Metadata metadata = generateMetadataFromThrowable(cancellationCause); - call.close(new ServerStatusAndMetadata( - fromThrowable(ctx, call.exceptionHandler(), cancellationCause, metadata), - metadata, true, true)); + final StatusAndMetadata statusAndMetadata = call.exceptionHandler().handle(ctx, cancellationCause); + call.close(new ServerStatusAndMetadata(statusAndMetadata.status(), statusAndMetadata.metadata(), + true)); return null; }); } @@ -339,9 +340,15 @@ private AbstractServerCall newServerCall( ServiceRequestContext ctx, HttpRequest req, HttpResponse res, @Nullable CompletableFuture resFuture, SerializationFormat serializationFormat, @Nullable Executor blockingExecutor) { + final MethodDescriptor methodDescriptor = methodDef.getMethodDescriptor(); - final GrpcExceptionHandlerFunction exceptionHandler = registry.getExceptionHandler( - methodDef); + final InternalGrpcExceptionHandler exceptionHandler = + registry.getExceptionHandler(methodDef); + assert exceptionHandler != null; + final GrpcJsonMarshaller jsonMarshaller = jsonMarshallers.get(methodDescriptor.getServiceName()); + final ResponseHeaders defaultHeaders = this.defaultHeaders.get(serializationFormat); + assert defaultHeaders != null; + if (methodDescriptor.getType() == MethodType.UNARY) { assert resFuture != null; return new UnaryServerCall<>( @@ -356,9 +363,9 @@ private AbstractServerCall newServerCall( maxResponseMessageLength, ctx, serializationFormat, - jsonMarshallers.get(methodDescriptor.getServiceName()), + jsonMarshaller, unsafeWrapRequestBuffers, - defaultHeaders.get(serializationFormat), + defaultHeaders, exceptionHandler, blockingExecutor, autoCompression, @@ -375,9 +382,9 @@ private AbstractServerCall newServerCall( maxResponseMessageLength, ctx, serializationFormat, - jsonMarshallers.get(methodDescriptor.getServiceName()), + jsonMarshaller, unsafeWrapRequestBuffers, - defaultHeaders.get(serializationFormat), + defaultHeaders, exceptionHandler, blockingExecutor, autoCompression, @@ -412,6 +419,7 @@ public void serviceAdded(ServiceConfig cfg) { } } + @Nullable @Override public ServerMethodDefinition methodDefinition(ServiceRequestContext ctx) { // method could be set in HttpJsonTranscodingService. diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/GrpcDecoratingService.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/GrpcDecoratingService.java index 6aa73eee97d..25fa68d7d30 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/GrpcDecoratingService.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/GrpcDecoratingService.java @@ -100,6 +100,7 @@ public List services() { return delegate.services(); } + @Nullable @Override public ServerMethodDefinition methodDefinition(ServiceRequestContext ctx) { return delegate.methodDefinition(ctx); diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/HandlerRegistry.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/HandlerRegistry.java index 6044ef4c969..7fa61ae94be 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/HandlerRegistry.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/HandlerRegistry.java @@ -47,6 +47,7 @@ package com.linecorp.armeria.server.grpc; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import static java.util.Objects.requireNonNull; import static org.reflections.ReflectionUtils.withModifier; @@ -71,6 +72,7 @@ import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.internal.common.ReflectiveDependencyInjector; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import com.linecorp.armeria.internal.server.annotation.AnnotationUtil; import com.linecorp.armeria.internal.server.annotation.DecoratorAnnotationUtil; import com.linecorp.armeria.internal.server.annotation.DecoratorAnnotationUtil.DecoratorAndOrder; @@ -100,10 +102,9 @@ final class HandlerRegistry { private final Map, List>> additionalDecorators; private final Set> blockingMethods; - private final Map, GrpcExceptionHandlerFunction> grpcExceptionHandlers; + private final Map, InternalGrpcExceptionHandler> grpcExceptionHandlers; - @Nullable - private final GrpcExceptionHandlerFunction defaultExceptionHandler; + private final InternalGrpcExceptionHandler defaultExceptionHandler; private HandlerRegistry(List services, Map> methods, @@ -115,7 +116,7 @@ private HandlerRegistry(List services, Set> blockingMethods, Map, GrpcExceptionHandlerFunction> grpcExceptionHandlers, - @Nullable GrpcExceptionHandlerFunction defaultExceptionHandler) { + GrpcExceptionHandlerFunction defaultExceptionHandler) { this.services = requireNonNull(services, "services"); this.methods = requireNonNull(methods, "methods"); this.methodsByRoute = requireNonNull(methodsByRoute, "methodsByRoute"); @@ -123,8 +124,14 @@ private HandlerRegistry(List services, this.annotationDecorators = requireNonNull(annotationDecorators, "annotationDecorators"); this.additionalDecorators = requireNonNull(additionalDecorators, "additionalDecorators"); this.blockingMethods = requireNonNull(blockingMethods, "blockingMethods"); - this.grpcExceptionHandlers = requireNonNull(grpcExceptionHandlers, "grpcExceptionHandlers"); - this.defaultExceptionHandler = defaultExceptionHandler; + requireNonNull(grpcExceptionHandlers, "grpcExceptionHandlers"); + this.grpcExceptionHandlers = + grpcExceptionHandlers.entrySet() + .stream() + .collect(toImmutableMap(Map.Entry::getKey, + e -> new InternalGrpcExceptionHandler( + e.getValue()))); + this.defaultExceptionHandler = new InternalGrpcExceptionHandler(defaultExceptionHandler); } @Nullable @@ -132,8 +139,10 @@ private HandlerRegistry(List services, return methods.get(methodName); } - String simpleMethodName(MethodDescriptor methodName) { - return simpleMethodNames.get(methodName); + String simpleMethodName(MethodDescriptor methodDescriptor) { + final String simpleMethodName = simpleMethodNames.get(methodDescriptor); + assert simpleMethodName != null : "No simple name found for " + methodDescriptor.getFullMethodName(); + return simpleMethodName; } List services() { @@ -161,12 +170,8 @@ boolean needToUseBlockingTaskExecutor(ServerMethodDefinition methodDef) { return blockingMethods.contains(methodDef); } - @Nullable - GrpcExceptionHandlerFunction getExceptionHandler(ServerMethodDefinition methodDef) { - if (!grpcExceptionHandlers.containsKey(methodDef)) { - return defaultExceptionHandler; - } - return grpcExceptionHandlers.get(methodDef); + InternalGrpcExceptionHandler getExceptionHandler(ServerMethodDefinition methodDef) { + return grpcExceptionHandlers.getOrDefault(methodDef, defaultExceptionHandler); } Map, HttpService> applyDecorators( @@ -269,7 +274,7 @@ private static void putGrpcExceptionHandlerIfPresent( ServerMethodDefinition methodDefinition, final ImmutableMap.Builder, GrpcExceptionHandlerFunction> grpcExceptionHandlersBuilder, - @Nullable GrpcExceptionHandlerFunction defaultExceptionHandler) { + GrpcExceptionHandlerFunction defaultExceptionHandler) { final List exceptionHandlers = AnnotationUtil.getAnnotatedInstances(method, clazz, GrpcExceptionHandler.class, @@ -279,11 +284,8 @@ private static void putGrpcExceptionHandlerIfPresent( exceptionHandlers.stream().reduce(GrpcExceptionHandlerFunction::orElse); grpcExceptionHandler.ifPresent(exceptionHandler -> { - GrpcExceptionHandlerFunction grpcExceptionHandler0 = exceptionHandler; - if (defaultExceptionHandler != null) { - grpcExceptionHandler0 = exceptionHandler.orElse(defaultExceptionHandler); - } - grpcExceptionHandlersBuilder.put(methodDefinition, grpcExceptionHandler0); + exceptionHandler = exceptionHandler.orElse(defaultExceptionHandler); + grpcExceptionHandlersBuilder.put(methodDefinition, exceptionHandler); }); } @@ -292,6 +294,9 @@ List entries() { } HandlerRegistry build() { + // setDefaultExceptionHandler() must be called before invoking build() + assert defaultExceptionHandler != null; + // Store per-service first, to make sure services are added/replaced atomically. final ImmutableMap.Builder services = ImmutableMap.builder(); final ImmutableMap.Builder> methods = ImmutableMap.builder(); diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/HttpJsonTranscodingService.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/HttpJsonTranscodingService.java index 78f804bf00c..6aeb0051759 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/HttpJsonTranscodingService.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/HttpJsonTranscodingService.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Base64; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -45,6 +46,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.api.AnnotationsProto; +import com.google.api.HttpBody; import com.google.api.HttpRule; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CaseFormat; @@ -78,7 +80,9 @@ import com.google.protobuf.Value; import com.linecorp.armeria.common.AggregatedHttpRequest; +import com.linecorp.armeria.common.AggregatedHttpResponse; import com.linecorp.armeria.common.HttpData; +import com.linecorp.armeria.common.HttpHeaderNames; import com.linecorp.armeria.common.HttpMethod; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpResponse; @@ -87,6 +91,7 @@ import com.linecorp.armeria.common.QueryParams; import com.linecorp.armeria.common.RequestHeaders; import com.linecorp.armeria.common.RequestHeadersBuilder; +import com.linecorp.armeria.common.ResponseHeaders; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.grpc.protocol.GrpcHeaderNames; @@ -107,6 +112,7 @@ import com.linecorp.armeria.server.grpc.HttpJsonTranscodingPathParser.VariablePathSegment; import com.linecorp.armeria.server.grpc.HttpJsonTranscodingPathParser.VerbPathSegment; import com.linecorp.armeria.server.grpc.HttpJsonTranscodingService.PathVariable.ValueDefinition.Type; +import com.linecorp.armeria.unsafe.PooledObjects; import io.grpc.MethodDescriptor.MethodType; import io.grpc.ServerMethodDefinition; @@ -153,10 +159,12 @@ static GrpcService of(GrpcService delegate, HttpJsonTranscodingOptions httpJsonT final HttpRule httpRule = methodOptions.getExtension(AnnotationsProto.http); - checkArgument(methodDefinition.getMethodDescriptor().getType() == MethodType.UNARY, - "Only unary methods can be configured with an HTTP/JSON endpoint: " + - "method=%s, httpRule=%s", - methodDefinition.getMethodDescriptor().getFullMethodName(), httpRule); + if (methodDefinition.getMethodDescriptor().getType() != MethodType.UNARY) { + logger.warn("Only unary methods can be configured with an HTTP/JSON endpoint: " + + "method={}, httpRule={}", + methodDefinition.getMethodDescriptor().getFullMethodName(), httpRule); + continue; + } @Nullable final Entry> routeAndVariables = toRouteAndPathVariables(httpRule); @@ -195,11 +203,11 @@ static GrpcService of(GrpcService delegate, HttpJsonTranscodingOptions httpJsonT = toRouteAndPathVariables(additionalHttpRule); if (additionalRouteAndVariables != null) { specs.put(additionalRouteAndVariables.getKey(), - new TranscodingSpec(order++, additionalHttpRule, methodDefinition, - serviceDesc, methodDesc, originalFields, - camelCaseFields, - additionalRouteAndVariables.getValue(), - responseBody)); + new TranscodingSpec(order++, additionalHttpRule, methodDefinition, + serviceDesc, methodDesc, originalFields, + camelCaseFields, + additionalRouteAndVariables.getValue(), + responseBody)); } } } @@ -435,7 +443,7 @@ private static String getResponseBody(List topLevelFields, if (StringUtil.isNullOrEmpty(responseBody)) { return null; } - for (FieldDescriptor fieldDescriptor: topLevelFields) { + for (FieldDescriptor fieldDescriptor : topLevelFields) { if (fieldDescriptor.getName().equals(responseBody)) { return responseBody; } @@ -444,41 +452,93 @@ private static String getResponseBody(List topLevelFields, } @Nullable - private static Function generateResponseBodyConverter(TranscodingSpec spec) { - @Nullable final String responseBody = spec.responseBody; + private static Function generateResponseConverter( + TranscodingSpec spec) { + // Ignore the spec if the method is HttpBody. The response body is already in the correct format. + if (HttpBody.getDescriptor().equals(spec.methodDescriptor.getOutputType())) { + return httpResponse -> { + final HttpData data = httpResponse.content(); + final JsonNode jsonNode = extractHttpBody(data); + + // Failed to parse the JSON body, return the original response. + if (jsonNode == null) { + return httpResponse; + } + + PooledObjects.close(data); + + // The data field is base64 encoded. + // https://protobuf.dev/programming-guides/proto3/#json + final String httpBody = jsonNode.get("data").asText(); + final byte[] httpBodyBytes = Base64.getDecoder().decode(httpBody); + + final ResponseHeaders newHeaders = httpResponse.headers().withMutations(builder -> { + final JsonNode contentType = jsonNode.get("contentType"); + + if (contentType != null && contentType.isTextual()) { + builder.set(HttpHeaderNames.CONTENT_TYPE, contentType.textValue()); + } else { + builder.remove(HttpHeaderNames.CONTENT_TYPE); + } + }); + + return AggregatedHttpResponse.of(newHeaders, HttpData.wrap(httpBodyBytes)); + }; + } + + @Nullable + final String responseBody = spec.responseBody; if (responseBody == null) { return null; - } else { - return httpData -> { - try (HttpData data = httpData) { - final byte[] array = data.array(); - try { - final JsonNode jsonNode = mapper.readValue(array, JsonNode.class); - // we try to convert lower snake case response body to camel case - final String lowerCamelCaseResponseBody = - CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, responseBody); - final Iterator> fields = jsonNode.fields(); - while (fields.hasNext()) { - final Entry entry = fields.next(); - final String fieldName = entry.getKey(); - final JsonNode responseBodyJsonNode = entry.getValue(); - // try to match field name and response body - // 1. by default the marshaller would use lowerCamelCase in json field - // 2. when the marshaller use original name in .proto file when serializing messages - if (fieldName.equals(lowerCamelCaseResponseBody) || - fieldName.equals(responseBody)) { - final byte[] bytes = mapper.writeValueAsBytes(responseBodyJsonNode); - return HttpData.wrap(bytes); - } - } - return HttpData.ofUtf8("null"); - } catch (IOException e) { - logger.warn("Unexpected exception while extracting responseBody '{}' from {}", - responseBody, data, e); - return HttpData.wrap(array); - } + } + + return httpResponse -> { + try (HttpData data = httpResponse.content()) { + final HttpData convertedData = convertHttpDataForResponseBody(responseBody, data); + return AggregatedHttpResponse.of(httpResponse.headers(), convertedData); + } + }; + } + + @Nullable + private static JsonNode extractHttpBody(HttpData data) { + final byte[] array = data.array(); + + try { + return mapper.readValue(array, JsonNode.class); + } catch (IOException e) { + logger.warn("Unexpected exception while parsing HttpBody from {}", data, e); + return null; + } + } + + private static HttpData convertHttpDataForResponseBody(String responseBody, HttpData data) { + final byte[] array = data.array(); + try { + final JsonNode jsonNode = mapper.readValue(array, JsonNode.class); + + // we try to convert lower snake case response body to camel case + final String lowerCamelCaseResponseBody = + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, responseBody); + final Iterator> fields = jsonNode.fields(); + while (fields.hasNext()) { + final Entry entry = fields.next(); + final String fieldName = entry.getKey(); + final JsonNode responseBodyJsonNode = entry.getValue(); + // try to match field name and response body + // 1. by default the marshaller would use lowerCamelCase in json field + // 2. when the marshaller use original name in .proto file when serializing messages + if (fieldName.equals(lowerCamelCaseResponseBody) || + fieldName.equals(responseBody)) { + final byte[] bytes = mapper.writeValueAsBytes(responseBodyJsonNode); + return HttpData.wrap(bytes); } - }; + } + return HttpData.ofUtf8("null"); + } catch (IOException e) { + logger.warn("Unexpected exception while extracting responseBody '{}' from {}", + responseBody, data, e); + return HttpData.wrap(array); } } @@ -506,6 +566,7 @@ private HttpJsonTranscodingService(GrpcService delegate, .contains(HttpJsonTranscodingQueryParamMatchRule.ORIGINAL_FIELD); } + @Nullable @Override public HttpEndpointSpecification httpEndpointSpecification(Route route) { requireNonNull(route, "route"); @@ -537,6 +598,7 @@ public Set routes() { return routes; } + @Nullable @Override public ServerMethodDefinition methodDefinition(ServiceRequestContext ctx) { final TranscodingSpec spec = routeAndSpecs.get(ctx.config().mappedRoute()); @@ -582,10 +644,20 @@ private HttpResponse serve0(ServiceRequestContext ctx, HttpRequest req, } else { try { ctx.setAttr(FramedGrpcService.RESOLVED_GRPC_METHOD, spec.method); - // Set JSON media type (https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#sending-arbitrary-content) + final HttpData requestContent; + + // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#sending-arbitrary-content + if (HttpBody.getDescriptor().equals(spec.methodDescriptor.getInputType())) { + // Convert the HTTP request to a JSON representation of HttpBody. + requestContent = convertToHttpBody(clientRequest); + } else { + // Convert the HTTP request to gRPC JSON. + requestContent = convertToJson(ctx, clientRequest, spec); + } + frameAndServe(unwrap(), ctx, grpcHeaders.build(), - convertToJson(ctx, clientRequest, spec), responseFuture, - generateResponseBodyConverter(spec), MediaType.JSON_UTF_8); + requestContent, responseFuture, + generateResponseConverter(spec)); } catch (IllegalArgumentException iae) { responseFuture.completeExceptionally( HttpStatusException.of(HttpStatus.BAD_REQUEST, iae)); @@ -599,6 +671,29 @@ private HttpResponse serve0(ServiceRequestContext ctx, HttpRequest req, return HttpResponse.of(responseFuture); } + private static HttpData convertToHttpBody(AggregatedHttpRequest request) throws IOException { + final ObjectNode body = mapper.createObjectNode(); + + try (HttpData content = request.content()) { + final MediaType contentType; + + @Nullable + final MediaType requestContentType = request.contentType(); + if (requestContentType != null) { + contentType = requestContentType; + } else { + contentType = MediaType.OCTET_STREAM; + } + + body.put("content_type", contentType.toString()); + // Jackson converts byte array to base64 string. gRPC transcoding spec also returns base64 string. + // https://protobuf.dev/programming-guides/proto3/#json + body.put("data", content.array()); + + return HttpData.wrap(mapper.writeValueAsBytes(body)); + } + } + /** * Converts the HTTP request to gRPC JSON with the {@link TranscodingSpec}. */ @@ -625,7 +720,7 @@ private HttpData convertToJson(ServiceRequestContext ctx, root = mapper.createObjectNode(); } else { throw new IllegalArgumentException("Unexpected JSON: " + - body + ", (expected: ObjectNode or null)."); + body + ", (expected: ObjectNode or null)."); } return setParametersAndWriteJson(root, ctx, spec); } diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/JsonUnframedGrpcErrorHandler.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/JsonUnframedGrpcErrorHandler.java index e3b3b7d0137..571d2877510 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/JsonUnframedGrpcErrorHandler.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/JsonUnframedGrpcErrorHandler.java @@ -135,6 +135,7 @@ public HttpResponse handle(ServiceRequestContext ctx, Status status, AggregatedH final String grpcMessage = status.getDescription(); final Throwable cause = responseCause(ctx); final HttpStatus httpStatus = statusMappingFunction.apply(ctx, status, cause); + assert httpStatus != null : "Default statusMappingFunction returned null?"; final HttpHeaders trailers = !response.trailers().isEmpty() ? response.trailers() : response.headers(); final String grpcStatusDetailsBin = trailers.get(GrpcHeaderNames.GRPC_STATUS_DETAILS_BIN); diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/StreamingServerCall.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/StreamingServerCall.java index 61833562348..5d5611be04a 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/StreamingServerCall.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/StreamingServerCall.java @@ -31,7 +31,6 @@ import com.linecorp.armeria.common.ResponseHeaders; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.annotation.Nullable; -import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.GrpcJsonMarshaller; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.grpc.protocol.DeframedMessage; @@ -40,6 +39,7 @@ import com.linecorp.armeria.common.stream.SubscriptionOption; import com.linecorp.armeria.internal.common.grpc.GrpcLogUtil; import com.linecorp.armeria.internal.common.grpc.HttpStreamDeframer; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import com.linecorp.armeria.internal.common.grpc.TransportStatusListener; import com.linecorp.armeria.internal.server.grpc.AbstractServerCall; import com.linecorp.armeria.internal.server.grpc.ServerStatusAndMetadata; @@ -84,7 +84,7 @@ final class StreamingServerCall extends AbstractServerCall ServiceRequestContext ctx, SerializationFormat serializationFormat, @Nullable GrpcJsonMarshaller jsonMarshaller, boolean unsafeWrapRequestBuffers, ResponseHeaders defaultHeaders, - @Nullable GrpcExceptionHandlerFunction exceptionHandler, + InternalGrpcExceptionHandler exceptionHandler, @Nullable Executor blockingExecutor, boolean autoCompress, boolean useMethodMarshaller) { super(req, method, simpleMethodName, compressorRegistry, decompressorRegistry, res, @@ -210,8 +210,7 @@ public void doClose(ServerStatusAndMetadata statusAndMetadata) { trailersOnly = false; } else { // A stream was closed already. - statusAndMetadata.shouldCancel(); - statusAndMetadata.setResponseContent(true); + statusAndMetadata.shouldCancel(true); closeListener(statusAndMetadata); return; } @@ -225,7 +224,6 @@ public void doClose(ServerStatusAndMetadata statusAndMetadata) { res.close(); } } finally { - statusAndMetadata.setResponseContent(false); closeListener(statusAndMetadata); } } @@ -264,7 +262,7 @@ public void onError(Throwable t) { } @Override - public void transportReportStatus(Status status, Metadata metadata) { + public void transportReportStatus(Status status, @Nullable Metadata metadata) { // A server doesn't see trailers from the client so will never have Metadata here. if (isCloseCalled()) { @@ -274,6 +272,6 @@ public void transportReportStatus(Status status, Metadata metadata) { // failure there's no need to notify the server listener of it). return; } - closeListener(new ServerStatusAndMetadata(status, metadata, true, true)); + closeListener(new ServerStatusAndMetadata(status, metadata, true)); } } diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/TextUnframedGrpcErrorHandler.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/TextUnframedGrpcErrorHandler.java index 674703470bb..034c26c6d15 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/TextUnframedGrpcErrorHandler.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/TextUnframedGrpcErrorHandler.java @@ -76,6 +76,7 @@ public HttpResponse handle(ServiceRequestContext ctx, Status status, AggregatedH final String grpcMessage = status.getDescription(); final Throwable cause = responseCause(ctx); final HttpStatus httpStatus = statusMappingFunction.apply(ctx, status, cause); + assert httpStatus != null : "Default statusMappingFunction returned null?"; final ResponseHeaders responseHeaders = ResponseHeaders.builder(httpStatus) .contentType(MediaType.PLAIN_TEXT_UTF_8) .addInt(GrpcHeaderNames.GRPC_STATUS, diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/UnaryServerCall.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/UnaryServerCall.java index 6f8516f6d13..687e1692de4 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/UnaryServerCall.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/UnaryServerCall.java @@ -34,10 +34,11 @@ import com.linecorp.armeria.common.ResponseHeadersBuilder; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.annotation.Nullable; -import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.GrpcJsonMarshaller; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.internal.common.grpc.GrpcLogUtil; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; +import com.linecorp.armeria.internal.common.grpc.StatusAndMetadata; import com.linecorp.armeria.internal.server.grpc.AbstractServerCall; import com.linecorp.armeria.internal.server.grpc.ServerStatusAndMetadata; import com.linecorp.armeria.server.ServiceRequestContext; @@ -70,7 +71,7 @@ final class UnaryServerCall extends AbstractServerCall { ServiceRequestContext ctx, SerializationFormat serializationFormat, @Nullable GrpcJsonMarshaller jsonMarshaller, boolean unsafeWrapRequestBuffers, ResponseHeaders defaultHeaders, - @Nullable GrpcExceptionHandlerFunction exceptionHandler, + InternalGrpcExceptionHandler exceptionHandler, @Nullable Executor blockingExecutor, boolean autoCompress, boolean useMethodMarshaller) { @@ -142,11 +143,11 @@ public boolean isReady() { @Override public void doClose(ServerStatusAndMetadata statusAndMetadata) { - final ResponseHeaders responseHeaders = responseHeaders(); - final Status status = statusAndMetadata.status(); - final Metadata metadata = statusAndMetadata.metadata(); - final HttpResponse response; try { + final ResponseHeaders responseHeaders = responseHeaders(); + final Status status = statusAndMetadata.status(); + final Metadata metadata = statusAndMetadata.metadata(); + final HttpResponse response; if (status.isOk()) { assert responseHeaders != null; assert responseMessage != null; @@ -174,11 +175,18 @@ public void doClose(ServerStatusAndMetadata statusAndMetadata) { // Set responseContent before closing stream to use responseCause in error handling ctx.logBuilder().responseContent(GrpcLogUtil.rpcResponse(statusAndMetadata, responseMessage), null); - statusAndMetadata.setResponseContent(false); resFuture.complete(response); } catch (Exception ex) { - statusAndMetadata.shouldCancel(); - resFuture.completeExceptionally(ex); + final StatusAndMetadata statusAndMetadata0 = exceptionHandler().handle(ctx, ex); + final Status status = statusAndMetadata0.status(); + final Metadata metadata = statusAndMetadata0.metadata(); + assert metadata != null; + statusAndMetadata = new ServerStatusAndMetadata(status, metadata, true); + + final ResponseHeadersBuilder trailersBuilder = defaultResponseHeaders().toBuilder(); + final HttpResponse response = HttpResponse.of( + (ResponseHeaders) statusToTrailers(ctx, trailersBuilder, status, metadata)); + resFuture.complete(response); } finally { closeListener(statusAndMetadata); } diff --git a/grpc/src/main/java/com/linecorp/armeria/server/grpc/UnframedGrpcService.java b/grpc/src/main/java/com/linecorp/armeria/server/grpc/UnframedGrpcService.java index d945e368df9..aa69eff27bc 100644 --- a/grpc/src/main/java/com/linecorp/armeria/server/grpc/UnframedGrpcService.java +++ b/grpc/src/main/java/com/linecorp/armeria/server/grpc/UnframedGrpcService.java @@ -20,7 +20,9 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import com.linecorp.armeria.common.AggregatedHttpResponse; import com.linecorp.armeria.common.AggregationOptions; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpResponse; @@ -28,7 +30,9 @@ import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.RequestHeaders; import com.linecorp.armeria.common.RequestHeadersBuilder; +import com.linecorp.armeria.common.ResponseHeaders; import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.grpc.protocol.GrpcHeaderNames; import com.linecorp.armeria.common.logging.RequestLogProperty; @@ -73,6 +77,7 @@ final class UnframedGrpcService extends AbstractUnframedGrpcService { checkArgument(delegate.isFramed(), "Decorated service must be a framed GrpcService."); } + @Nullable @Override public ServerMethodDefinition methodDefinition(ServiceRequestContext ctx) { return delegate.methodDefinition(ctx); @@ -153,8 +158,15 @@ public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exc if (t != null) { responseFuture.completeExceptionally(t); } else { + // Add the content type to the response headers. + final Function responseConverter = + response -> { + final ResponseHeaders headers = response.headers().withMutations( + builder -> builder.contentType(contentType)); + return AggregatedHttpResponse.of(headers, response.content()); + }; frameAndServe(unwrap(), ctx, grpcHeaders.build(), clientRequest.content(), - responseFuture, null, contentType); + responseFuture, responseConverter); } } return null; diff --git a/grpc/src/test/java/com/linecorp/armeria/client/grpc/GrpcClientTest.java b/grpc/src/test/java/com/linecorp/armeria/client/grpc/GrpcClientTest.java index 1c0580317b2..9e6eda085b0 100644 --- a/grpc/src/test/java/com/linecorp/armeria/client/grpc/GrpcClientTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/client/grpc/GrpcClientTest.java @@ -85,6 +85,7 @@ import com.linecorp.armeria.common.util.ThreadFactories; import com.linecorp.armeria.internal.common.RequestTargetCache; import com.linecorp.armeria.internal.common.grpc.GrpcLogUtil; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import com.linecorp.armeria.internal.common.grpc.MetadataUtil; import com.linecorp.armeria.internal.common.grpc.StreamRecorder; import com.linecorp.armeria.internal.common.grpc.TestServiceImpl; @@ -175,10 +176,10 @@ public void onNext(SimpleRequest value) { } @Override - public void onError(Throwable t) { } + public void onError(Throwable t) {} @Override - public void onCompleted() { } + public void onCompleted() {} }; } @@ -238,6 +239,8 @@ public void close(Status status, Metadata trailers) { } }; + private final InternalGrpcExceptionHandler grpcExceptionHandler = + new InternalGrpcExceptionHandler(GrpcExceptionHandlerFunction.of()); private final BlockingQueue requestLogQueue = new LinkedTransferQueue<>(); private TestServiceBlockingStub blockingStub; private TestServiceStub asyncStub; @@ -318,9 +321,9 @@ void contextCaptorAsync() { void callOptionsExecutorIsRespected() { final ExecutorService executor = Executors.newSingleThreadExecutor( ThreadFactories.builder("my-calloptions-executor-3") - .daemon(true) - .eventLoop(false) - .build()); + .daemon(true) + .eventLoop(false) + .build()); final TestServiceGrpc.TestServiceStub asyncStubWithExecutor = asyncStub.withExecutor(executor); final AtomicReference onMessageThread = new AtomicReference<>(); @@ -746,9 +749,8 @@ void cancelAfterBegin() throws Exception { requestObserver.onError(new RuntimeException()); responseObserver.awaitCompletion(); assertThat(responseObserver.getValues()).isEmpty(); - assertThat(GrpcExceptionHandlerFunction.of() - .apply(null, Status.UNKNOWN, responseObserver.getError(), null) - .getCode()).isEqualTo(Code.CANCELLED); + assertThat(grpcExceptionHandler.handle(null, responseObserver.getError()).status().getCode()) + .isEqualTo(Code.CANCELLED); final RequestLog log = requestLogQueue.take(); assertThat(log.isComplete()).isTrue(); @@ -782,9 +784,8 @@ void cancelAfterFirstResponse() throws Exception { requestObserver.onError(new RuntimeException()); responseObserver.awaitCompletion(operationTimeoutMillis(), TimeUnit.MILLISECONDS); assertThat(responseObserver.getValues()).hasSize(1); - assertThat(GrpcExceptionHandlerFunction.of() - .apply(null, Status.UNKNOWN, responseObserver.getError(), null) - .getCode()).isEqualTo(Code.CANCELLED); + assertThat(grpcExceptionHandler.handle(null, responseObserver.getError()).status() + .getCode()).isEqualTo(Code.CANCELLED); checkRequestLog((rpcReq, rpcRes, grpcStatus) -> { assertThat(rpcReq.params()).containsExactly(request); @@ -1417,9 +1418,8 @@ void deadlineExceededServerStreaming() throws Exception { recorder.awaitCompletion(); assertThat(recorder.getError()).isNotNull(); - assertThat(GrpcExceptionHandlerFunction.of() - .apply(null, Status.UNKNOWN, recorder.getError(), null) - .getCode()) + assertThat(grpcExceptionHandler.handle(null, recorder.getError()).status() + .getCode()) .isEqualTo(Status.DEADLINE_EXCEEDED.getCode()); checkRequestLogError((headers, rpcReq, cause) -> { @@ -1617,12 +1617,11 @@ void statusCodeAndMessage() throws Exception { final ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); verify(responseObserver, timeout(operationTimeoutMillis())).onError(captor.capture()); - assertThat(GrpcExceptionHandlerFunction.of() - .apply(null, Status.UNKNOWN, captor.getValue(), null) - .getCode()).isEqualTo(Status.UNKNOWN.getCode()); - assertThat(GrpcExceptionHandlerFunction.of() - .apply(null, Status.UNKNOWN, captor.getValue(), null) - .getDescription()).isEqualTo(errorMessage); + + assertThat(grpcExceptionHandler.handle(null, captor.getValue()).status() + .getCode()).isEqualTo(Status.UNKNOWN.getCode()); + assertThat(grpcExceptionHandler.handle(null, captor.getValue()).status() + .getDescription()).isEqualTo(errorMessage); verifyNoMoreInteractions(responseObserver); checkRequestLog((rpcReq, rpcRes, grpcStatus) -> { diff --git a/grpc/src/test/java/com/linecorp/armeria/internal/common/grpc/HttpStreamDeframerTest.java b/grpc/src/test/java/com/linecorp/armeria/internal/common/grpc/HttpStreamDeframerTest.java index 5d68db290d4..f46f91781f8 100644 --- a/grpc/src/test/java/com/linecorp/armeria/internal/common/grpc/HttpStreamDeframerTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/internal/common/grpc/HttpStreamDeframerTest.java @@ -58,8 +58,8 @@ void setUp() { final ServiceRequestContext ctx = ServiceRequestContext.of(HttpRequest.of(HttpMethod.GET, "/")); final TransportStatusListener statusListener = (status, metadata) -> statusRef.set(status); deframer = new HttpStreamDeframer(DecompressorRegistry.getDefaultInstance(), ctx, statusListener, - GrpcExceptionHandlerFunction.of(), Integer.MAX_VALUE, - false, true); + new InternalGrpcExceptionHandler(GrpcExceptionHandlerFunction.of()), + Integer.MAX_VALUE, false, true); } @Test diff --git a/grpc/src/test/java/com/linecorp/armeria/it/grpc/HttpJsonTranscodingTest.java b/grpc/src/test/java/com/linecorp/armeria/it/grpc/HttpJsonTranscodingTest.java index eb93aef39b0..42889a6927e 100644 --- a/grpc/src/test/java/com/linecorp/armeria/it/grpc/HttpJsonTranscodingTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/it/grpc/HttpJsonTranscodingTest.java @@ -45,6 +45,7 @@ import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.api.HttpBody; import com.google.common.collect.ImmutableList; import com.google.common.collect.Streams; @@ -76,6 +77,8 @@ import io.grpc.stub.StreamObserver; import testing.grpc.HttpJsonTranscodingTestServiceGrpc.HttpJsonTranscodingTestServiceBlockingStub; import testing.grpc.HttpJsonTranscodingTestServiceGrpc.HttpJsonTranscodingTestServiceImplBase; +import testing.grpc.Transcoding.ArbitraryHttpWrappedRequest; +import testing.grpc.Transcoding.ArbitraryHttpWrappedResponse; import testing.grpc.Transcoding.EchoAnyRequest; import testing.grpc.Transcoding.EchoAnyResponse; import testing.grpc.Transcoding.EchoFieldMaskRequest; @@ -333,6 +336,25 @@ public void echoNestedMessageField(EchoNestedMessageRequest request, .onNext(EchoNestedMessageResponse.newBuilder().setNested(request.getNested()).build()); responseObserver.onCompleted(); } + + @Override + public void arbitraryHttp(HttpBody request, StreamObserver responseObserver) { + final HttpBody.Builder builder = HttpBody.newBuilder(); + builder.setContentType(request.getContentType()) + .setData(request.getData()); + responseObserver.onNext(builder.build()); + responseObserver.onCompleted(); + } + + @Override + public void arbitraryHttpWrapped(ArbitraryHttpWrappedRequest request, + StreamObserver responseObserver) { + final ArbitraryHttpWrappedResponse.Builder builder = ArbitraryHttpWrappedResponse.newBuilder(); + builder.setResponseId(request.getRequestId() + "-response"); + builder.setBody(request.getBody()); + responseObserver.onNext(builder.build()); + responseObserver.onCompleted(); + } } @RegisterExtension @@ -1085,6 +1107,19 @@ public static JsonNode findMethod(JsonNode methods, String name) { .findFirst().get(); } + @Test + void shouldAcceptArbitraryHttpUsingHttpBody() { + final String content = "Arbitrary HTTP body"; + final RequestHeaders headers = RequestHeaders.builder() + .method(HttpMethod.POST).path("/v1/arbitrary") + .contentType(MediaType.HTML_UTF_8).build(); + final AggregatedHttpResponse response = + webClient.execute(headers, content.getBytes()).aggregate().join(); + + assertThat(response.contentType()).isEqualTo(MediaType.HTML_UTF_8); + assertThat(response.contentUtf8()).isEqualTo(content); + } + public static List pathMapping(JsonNode method) { return Streams.stream(method.get("endpoints")).map(node -> node.get("pathMapping").asText()) .collect(toImmutableList()); diff --git a/grpc/src/test/java/com/linecorp/armeria/server/grpc/DeferredListenerTest.java b/grpc/src/test/java/com/linecorp/armeria/server/grpc/DeferredListenerTest.java index 47d9ea0f1c5..b69ff8ef5d1 100644 --- a/grpc/src/test/java/com/linecorp/armeria/server/grpc/DeferredListenerTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/server/grpc/DeferredListenerTest.java @@ -37,7 +37,9 @@ import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.common.ResponseHeaders; +import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import com.linecorp.armeria.server.ServiceRequestContext; import io.grpc.CompressorRegistry; @@ -112,7 +114,9 @@ private static UnaryServerCall newServerCall( DecompressorRegistry.getDefaultInstance(), HttpResponse.streaming(), new CompletableFuture<>(), 0, 0, ctx, GrpcSerializationFormats.PROTO, null, false, - ResponseHeaders.of(200), null, blockingTaskExecutor, false, false); + ResponseHeaders.of(200), + new InternalGrpcExceptionHandler(GrpcExceptionHandlerFunction.of()), + blockingTaskExecutor, false, false); } private static class TestListener extends ServerCall.Listener { diff --git a/grpc/src/test/java/com/linecorp/armeria/server/grpc/GrpcMessageLengthTest.java b/grpc/src/test/java/com/linecorp/armeria/server/grpc/GrpcMessageLengthTest.java new file mode 100644 index 00000000000..6e552802976 --- /dev/null +++ b/grpc/src/test/java/com/linecorp/armeria/server/grpc/GrpcMessageLengthTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.server.grpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.google.common.base.Strings; +import com.google.protobuf.ByteString; + +import com.linecorp.armeria.client.grpc.GrpcClients; +import com.linecorp.armeria.internal.common.grpc.TestServiceImpl; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; + +import io.grpc.Status.Code; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import testing.grpc.Messages.Payload; +import testing.grpc.Messages.ResponseParameters; +import testing.grpc.Messages.SimpleRequest; +import testing.grpc.Messages.StreamingOutputCallRequest; +import testing.grpc.Messages.StreamingOutputCallResponse; +import testing.grpc.TestServiceGrpc.TestServiceBlockingStub; + +class GrpcMessageLengthTest { + @RegisterExtension + static ServerExtension server = new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + final GrpcService grpcService = + GrpcService.builder() + .addService(new TestServiceImpl( + Executors.newSingleThreadScheduledExecutor())) + .maxRequestMessageLength(1000) + .maxResponseMessageLength(1000) + .exceptionHandler((ctx, status, cause, metadata) -> { + if (cause instanceof StatusRuntimeException) { + assertThat(((StatusRuntimeException) cause).getStatus().getCode()) + .isEqualTo(status.getCode()); + } else if (cause instanceof StatusException) { + assertThat(((StatusException) cause).getStatus().getCode()) + .isEqualTo(status.getCode()); + } + return status.withDescription( + status.getDescription() + ": exception handled"); + }) + .build(); + sb.service(grpcService); + } + }; + + @Test + void shouldHandleExceedingRequestLength() { + final TestServiceBlockingStub client = GrpcClients.builder(server.httpUri()) + .build(TestServiceBlockingStub.class); + final Payload payload = Payload.newBuilder() + .setBody(ByteString.copyFrom(Strings.repeat("a", 1001), + StandardCharsets.UTF_8)) + .build(); + final SimpleRequest request = SimpleRequest.newBuilder() + .setPayload(payload) + .build(); + assertResourceExhausted(() -> client.unaryCall(request)); + } + + @Test + void shouldHandleExceedingResponseLength() { + final TestServiceBlockingStub client = GrpcClients.builder(server.httpUri()) + .build(TestServiceBlockingStub.class); + final Payload payload = Payload.newBuilder() + .build(); + final SimpleRequest request = SimpleRequest.newBuilder() + .setResponseSize(1001) + .setPayload(payload) + .build(); + assertResourceExhausted(() -> client.unaryCall(request)); + } + + @Test + void shouldHandleExceedingRequestLength_streaming() { + final TestServiceBlockingStub client = GrpcClients.builder(server.httpUri()) + .build(TestServiceBlockingStub.class); + final Payload payload = Payload.newBuilder() + .setBody(ByteString.copyFrom(Strings.repeat("a", 1001), + StandardCharsets.UTF_8)) + .build(); + final StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() + .setPayload(payload) + .build(); + assertResourceExhausted(() -> { + final Iterator response = client.streamingOutputCall(request); + // Drain responses + while (response.hasNext()) { + response.next(); + } + }); + } + + @Test + void shouldHandleExceedingResponseLength_streaming() { + final TestServiceBlockingStub client = GrpcClients.builder(server.httpUri()) + .build(TestServiceBlockingStub.class); + final ResponseParameters parameters = ResponseParameters.newBuilder() + .setSize(1001) + .build(); + final StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() + .addResponseParameters(parameters) + .build(); + assertResourceExhausted(() -> { + final Iterator response = client.streamingOutputCall(request); + // Drain responses + while (response.hasNext()) { + response.next(); + } + }); + } + + private static void assertResourceExhausted(Runnable call) { + assertThatThrownBy(call::run) + .isInstanceOfSatisfying(StatusRuntimeException.class, cause -> { + assertThat(cause.getStatus().getCode()).isEqualTo(Code.RESOURCE_EXHAUSTED); + assertThat(cause.getMessage()).endsWith(": exception handled"); + }); + } +} diff --git a/grpc/src/test/java/com/linecorp/armeria/server/grpc/HandlerRegistryTest.java b/grpc/src/test/java/com/linecorp/armeria/server/grpc/HandlerRegistryTest.java index b8fb039fc82..ded63529dde 100644 --- a/grpc/src/test/java/com/linecorp/armeria/server/grpc/HandlerRegistryTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/server/grpc/HandlerRegistryTest.java @@ -21,6 +21,8 @@ import com.google.common.collect.ImmutableList; +import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; + import testing.grpc.TestServiceGrpc.TestServiceImplBase; class HandlerRegistryTest { @@ -33,7 +35,8 @@ class HandlerRegistryTest { }) @ParameterizedTest void normalizePath(String path1, String path2, String expected1, String expected2) { - final HandlerRegistry.Builder builder = new HandlerRegistry.Builder(); + final HandlerRegistry.Builder builder = new HandlerRegistry.Builder() + .setDefaultExceptionHandler(GrpcExceptionHandlerFunction.of()); final TestServiceImplBase testService = new TestServiceImplBase() {}; final HandlerRegistry handlerRegistry = builder.addService(path1, testService.bindService(), null, null, ImmutableList.of()) diff --git a/grpc/src/test/java/com/linecorp/armeria/server/grpc/GrpcExceptionHandlerFunctionUtilTest.java b/grpc/src/test/java/com/linecorp/armeria/server/grpc/InternalGrpcExceptionHandlerTest.java similarity index 60% rename from grpc/src/test/java/com/linecorp/armeria/server/grpc/GrpcExceptionHandlerFunctionUtilTest.java rename to grpc/src/test/java/com/linecorp/armeria/server/grpc/InternalGrpcExceptionHandlerTest.java index 5d4e594799e..8e5db1946ea 100644 --- a/grpc/src/test/java/com/linecorp/armeria/server/grpc/GrpcExceptionHandlerFunctionUtilTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/server/grpc/InternalGrpcExceptionHandlerTest.java @@ -26,10 +26,12 @@ import org.junit.jupiter.params.provider.CsvSource; import com.linecorp.armeria.client.grpc.GrpcClients; +import com.linecorp.armeria.common.grpc.protocol.ArmeriaStatusException; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.testing.junit5.server.ServerExtension; import io.grpc.Status; +import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; import testing.grpc.Messages.SimpleRequest; @@ -38,7 +40,7 @@ import testing.grpc.TestServiceGrpc.TestServiceBlockingStub; import testing.grpc.TestServiceGrpc.TestServiceImplBase; -class GrpcExceptionHandlerFunctionUtilTest { +class InternalGrpcExceptionHandlerTest { @RegisterExtension static final ServerExtension server = new ServerExtension() { @@ -51,6 +53,19 @@ protected void configure(ServerBuilder sb) throws Exception { return Status.INTERNAL; }) .build()); + + sb.serviceUnder("/status", + GrpcService.builder() + .addService(new TestServiceImpl()) + .exceptionHandler((ctx, status, throwable, metadata) -> { + assertThat(throwable).isInstanceOf(StatusRuntimeException.class); + assertThat(status.getCode()) + .isEqualTo(((StatusRuntimeException) throwable).getStatus() + .getCode()); + // Delegate to the default exception handler + return null; + }) + .build()); } }; @@ -70,6 +85,25 @@ void classAndMethodHaveMultipleExceptionHandlers(String exceptionType) { e -> assertThat(e.getStatus()).isEqualTo(Status.INTERNAL)); } + @CsvSource({ "onError", "throw" }) + @ParameterizedTest + void shouldPreserveCodeInArmeriaStatusException(String exceptionType) { + final TestServiceBlockingStub client = + GrpcClients.builder(server.httpUri()) + .pathPrefix("/status") + .build(TestServiceBlockingStub.class); + + final SimpleRequest globalRequest = + SimpleRequest.newBuilder() + .setNestedRequest(NestedRequest.newBuilder().setNestedPayload(exceptionType) + .build()) + .build(); + assertThatThrownBy(() -> client.unaryCall2(globalRequest)) + .isInstanceOfSatisfying(StatusRuntimeException.class, + e -> assertThat(e.getStatus().getCode()) + .isEqualTo(Code.FAILED_PRECONDITION)); + } + private static class TestServiceImpl extends TestServiceImplBase { @Override @@ -90,5 +124,20 @@ public void unaryCall(SimpleRequest request, StreamObserver resp throw new IllegalArgumentException("unknown payload"); } } + + @Override + public void unaryCall2(SimpleRequest request, StreamObserver responseObserver) { + final ArmeriaStatusException exception = + new ArmeriaStatusException(Code.FAILED_PRECONDITION.value(), "failed"); + switch (request.getNestedRequest().getNestedPayload()) { + case "onError": + responseObserver.onError(exception); + break; + case "throw": + throw exception; + default: + throw new IllegalArgumentException("unknown payload"); + } + } } } diff --git a/grpc/src/test/java/com/linecorp/armeria/server/grpc/StreamingServerCallTest.java b/grpc/src/test/java/com/linecorp/armeria/server/grpc/StreamingServerCallTest.java index 401fefb2725..4de8499142a 100644 --- a/grpc/src/test/java/com/linecorp/armeria/server/grpc/StreamingServerCallTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/server/grpc/StreamingServerCallTest.java @@ -54,11 +54,13 @@ import com.linecorp.armeria.common.HttpResponseWriter; import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.common.ResponseHeaders; +import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.grpc.protocol.DeframedMessage; import com.linecorp.armeria.common.util.EventLoopGroups; import com.linecorp.armeria.internal.common.grpc.DefaultJsonMarshaller; import com.linecorp.armeria.internal.common.grpc.GrpcTestUtil; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.unsafe.grpc.GrpcUnsafeBufferUtil; @@ -88,6 +90,9 @@ class StreamingServerCallTest { private static final Key EXTRA_HEADER_KEY1 = Key.of(EXTRA_HEADER_NAME1.toString(), Metadata.ASCII_STRING_MARSHALLER); + private static final InternalGrpcExceptionHandler exceptionHandler = + new InternalGrpcExceptionHandler(GrpcExceptionHandlerFunction.of()); + @Mock private HttpResponseWriter res; @@ -296,7 +301,7 @@ void deferResponseHeaders_streaming_nonResponseMessage() { ResponseHeaders.builder(HttpStatus.OK) .contentType(GrpcSerializationFormats.PROTO.mediaType()) .build(), - /* exceptionMappings */ null, + exceptionHandler, /* blockingExecutor */ null, false, false); @@ -366,7 +371,7 @@ private StreamingServerCall newServerCall(HttpRes ResponseHeaders.builder(HttpStatus.OK) .contentType(GrpcSerializationFormats.PROTO.mediaType()) .build(), - /* exceptionMappings */ null, + exceptionHandler, /* blockingExecutor */ null, false, false); diff --git a/grpc/src/test/java/com/linecorp/armeria/server/grpc/UnaryServerCallTest.java b/grpc/src/test/java/com/linecorp/armeria/server/grpc/UnaryServerCallTest.java index da838a1c01a..6adde1b23d2 100644 --- a/grpc/src/test/java/com/linecorp/armeria/server/grpc/UnaryServerCallTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/server/grpc/UnaryServerCallTest.java @@ -52,11 +52,13 @@ import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.common.RequestHeaders; import com.linecorp.armeria.common.ResponseHeaders; +import com.linecorp.armeria.common.grpc.GrpcExceptionHandlerFunction; import com.linecorp.armeria.common.grpc.GrpcSerializationFormats; import com.linecorp.armeria.common.grpc.protocol.DeframedMessage; import com.linecorp.armeria.common.util.EventLoopGroups; import com.linecorp.armeria.internal.common.grpc.DefaultJsonMarshaller; import com.linecorp.armeria.internal.common.grpc.GrpcTestUtil; +import com.linecorp.armeria.internal.common.grpc.InternalGrpcExceptionHandler; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.unsafe.grpc.GrpcUnsafeBufferUtil; @@ -81,6 +83,9 @@ class UnaryServerCallTest { private static final Key EXTRA_HEADER_KEY1 = Key.of(EXTRA_HEADER_NAME1.toString(), Metadata.ASCII_STRING_MARSHALLER); + private static final InternalGrpcExceptionHandler exceptionHandler = + new InternalGrpcExceptionHandler(GrpcExceptionHandlerFunction.of()); + private HttpResponse res; @Mock @@ -329,7 +334,7 @@ void decodeMultipleChunks() { ResponseHeaders.builder(HttpStatus.OK) .contentType(GrpcSerializationFormats.PROTO.mediaType()) .build(), - /* exceptionMappings */ null, + exceptionHandler, /* blockingExecutor */ null, /* autoCompress */ false, /* useMethodMarshaller */ false); @@ -375,7 +380,7 @@ private UnaryServerCall newServerCall( ResponseHeaders.builder(HttpStatus.OK) .contentType(GrpcSerializationFormats.PROTO.mediaType()) .build(), - /* exceptionMappings */ null, + exceptionHandler, /* blockingExecutor */ null, /* autoCompress */ false, /* useMethodMarshaller */ false); diff --git a/grpc/src/test/java/com/linecorp/armeria/server/grpc/UnframedGrpcServiceTest.java b/grpc/src/test/java/com/linecorp/armeria/server/grpc/UnframedGrpcServiceTest.java index e6bf05beb6d..af4957bdffd 100644 --- a/grpc/src/test/java/com/linecorp/armeria/server/grpc/UnframedGrpcServiceTest.java +++ b/grpc/src/test/java/com/linecorp/armeria/server/grpc/UnframedGrpcServiceTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.spy; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -116,6 +117,11 @@ public void emptyCall(Empty request, StreamObserver responseObserver) { .startsWith("grpc-code: CANCELLED, Completed without a response"); } + private static Function contentType(MediaType type) { + return response -> AggregatedHttpResponse.of(response.headers().toBuilder().contentType(type).build(), + response.content()); + } + @Test void shouldClosePooledObjectsForNonOK() { final CompletableFuture res = new CompletableFuture<>(); @@ -127,7 +133,7 @@ void shouldClosePooledObjectsForNonOK() { final AggregatedHttpResponse framedResponse = AggregatedHttpResponse.of(responseHeaders, HttpData.wrap(byteBuf)); UnframedGrpcService.deframeAndRespond(ctx, framedResponse, res, UnframedGrpcErrorHandler.of(), - null, MediaType.PROTOBUF); + contentType(MediaType.PROTOBUF)); assertThat(byteBuf.refCnt()).isZero(); } @@ -141,7 +147,7 @@ void shouldClosePooledObjectsForMissingMediaType() { final AggregatedHttpResponse framedResponse = AggregatedHttpResponse .of(responseHeaders, HttpData.wrap(byteBuf)); AbstractUnframedGrpcService.deframeAndRespond(ctx, framedResponse, res, UnframedGrpcErrorHandler.of(), - null, MediaType.PROTOBUF); + contentType(MediaType.PROTOBUF)); assertThat(byteBuf.refCnt()).isZero(); } @@ -155,7 +161,7 @@ void shouldClosePooledObjectsForMissingGrpcStatus() { final AggregatedHttpResponse framedResponse = AggregatedHttpResponse.of(responseHeaders, HttpData.wrap(byteBuf)); AbstractUnframedGrpcService.deframeAndRespond(ctx, framedResponse, res, UnframedGrpcErrorHandler.of(), - null, MediaType.PROTOBUF); + contentType(MediaType.PROTOBUF)); assertThat(byteBuf.refCnt()).isZero(); } @@ -170,7 +176,7 @@ void succeedWithAllRequiredHeaders() throws Exception { final AggregatedHttpResponse framedResponse = AggregatedHttpResponse .of(responseHeaders, HttpData.wrap(byteBuf)); AbstractUnframedGrpcService.deframeAndRespond(ctx, framedResponse, res, UnframedGrpcErrorHandler.of(), - null, MediaType.PROTOBUF); + contentType(MediaType.PROTOBUF)); assertThat(HttpResponse.of(res).aggregate().get().status()).isEqualTo(HttpStatus.OK); } diff --git a/grpc/src/test/proto/testing/grpc/transcoding.proto b/grpc/src/test/proto/testing/grpc/transcoding.proto index 1e7c832bd51..b4d586ceff7 100644 --- a/grpc/src/test/proto/testing/grpc/transcoding.proto +++ b/grpc/src/test/proto/testing/grpc/transcoding.proto @@ -27,6 +27,7 @@ import "google/protobuf/wrappers.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/any.proto"; import "google/protobuf/field_mask.proto"; +import "google/api/httpbody.proto"; service HttpJsonTranscodingTestService { rpc GetMessageV1(GetMessageRequestV1) returns (Message) { @@ -224,6 +225,24 @@ service HttpJsonTranscodingTestService { } }; } + + rpc ArbitraryHttp(google.api.HttpBody) returns (google.api.HttpBody) { + option (google.api.http) = { + post: "/v1/arbitrary" + }; + } + + rpc ArbitraryHttpWrapped(ArbitraryHttpWrappedRequest) returns (ArbitraryHttpWrappedResponse) { + option (google.api.http) = { + post: "/v1/arbitrary_wrapped" + }; + } + + rpc EchoBidirectionalStream(stream Message) returns (stream Message) { + option (google.api.http) = { + post: "/v1/echo/bidirectional_stream" + }; + } } message GetMessageRequestV1 { @@ -407,3 +426,13 @@ message EchoNestedMessageRequest { message EchoNestedMessageResponse { TopLevelMessage.NestedMessage nested = 1; } + +message ArbitraryHttpWrappedRequest { + string request_id = 1; + google.api.HttpBody body = 2; +} + +message ArbitraryHttpWrappedResponse { + string response_id = 1; + google.api.HttpBody body = 2; +} diff --git a/it/kubernetes-chaos-tests/src/main/java/com/linecorp/armeria/kubernetes/it/CheckerCommand.java b/it/kubernetes-chaos-tests/src/main/java/com/linecorp/armeria/kubernetes/it/CheckerCommand.java index e6f8d0d4d2c..d85d36f7337 100644 --- a/it/kubernetes-chaos-tests/src/main/java/com/linecorp/armeria/kubernetes/it/CheckerCommand.java +++ b/it/kubernetes-chaos-tests/src/main/java/com/linecorp/armeria/kubernetes/it/CheckerCommand.java @@ -48,6 +48,7 @@ import picocli.CommandLine; import picocli.CommandLine.Command; +@SuppressWarnings("NullAway") @Command(name = "checker", mixinStandardHelpOptions = true) public class CheckerCommand implements Runnable { diff --git a/it/kubernetes-chaos-tests/src/main/java/com/linecorp/armeria/kubernetes/it/ControlCommand.java b/it/kubernetes-chaos-tests/src/main/java/com/linecorp/armeria/kubernetes/it/ControlCommand.java index 246a8d98f1c..6106478f771 100644 --- a/it/kubernetes-chaos-tests/src/main/java/com/linecorp/armeria/kubernetes/it/ControlCommand.java +++ b/it/kubernetes-chaos-tests/src/main/java/com/linecorp/armeria/kubernetes/it/ControlCommand.java @@ -44,6 +44,7 @@ import picocli.CommandLine; import picocli.CommandLine.Command; +@SuppressWarnings("NullAway") @Command(name = "control", mixinStandardHelpOptions = true) public class ControlCommand implements Runnable { diff --git a/jetty/jetty11/src/main/java/com/linecorp/armeria/server/jetty/JettyService.java b/jetty/jetty11/src/main/java/com/linecorp/armeria/server/jetty/JettyService.java index d0d26be85da..8413b5b3aa3 100644 --- a/jetty/jetty11/src/main/java/com/linecorp/armeria/server/jetty/JettyService.java +++ b/jetty/jetty11/src/main/java/com/linecorp/armeria/server/jetty/JettyService.java @@ -411,6 +411,7 @@ public void send(MetaData.Request unused, MetaData.@Nullable Response response, final int length = content != null ? content.remaining() : 0; if (ctx.request().headers().method() != HttpMethod.HEAD && length != 0) { final HttpData data; + assert content != null; if (content.hasArray()) { final int from = content.arrayOffset() + content.position(); content.position(content.position() + length); diff --git a/jetty/jetty9/src/main/java/com/linecorp/armeria/server/jetty/JettyService.java b/jetty/jetty9/src/main/java/com/linecorp/armeria/server/jetty/JettyService.java index 824e27a5843..ec38c96e23c 100644 --- a/jetty/jetty9/src/main/java/com/linecorp/armeria/server/jetty/JettyService.java +++ b/jetty/jetty9/src/main/java/com/linecorp/armeria/server/jetty/JettyService.java @@ -421,6 +421,7 @@ public void send(MetaData.@Nullable Response info, boolean head, final int length = content != null ? content.remaining() : 0; if (!head && length != 0) { final HttpData data; + assert content != null; if (content.hasArray()) { final int from = content.arrayOffset() + content.position(); content.position(content.position() + length); diff --git a/junit5/src/main/java/com/linecorp/armeria/internal/testing/SelfSignedCertificateRuleDelegate.java b/junit5/src/main/java/com/linecorp/armeria/internal/testing/SelfSignedCertificateRuleDelegate.java index 94bd4674002..56ecf114893 100644 --- a/junit5/src/main/java/com/linecorp/armeria/internal/testing/SelfSignedCertificateRuleDelegate.java +++ b/junit5/src/main/java/com/linecorp/armeria/internal/testing/SelfSignedCertificateRuleDelegate.java @@ -181,35 +181,32 @@ public void after() { * Returns the generated {@link X509Certificate}. */ public X509Certificate certificate() { - ensureCertificate(); - return certificate.cert(); + return ensureCertificate().cert(); } /** * Returns the self-signed certificate file. */ public File certificateFile() { - ensureCertificate(); - return certificate.certificate(); + return ensureCertificate().certificate(); } /** * Returns the {@link PrivateKey} of the self-signed certificate. */ public PrivateKey privateKey() { - ensureCertificate(); - return certificate.key(); + return ensureCertificate().key(); } /** * Returns the private key file of the self-signed certificate. */ public File privateKeyFile() { - ensureCertificate(); - return certificate.privateKey(); + return ensureCertificate().privateKey(); } - private void ensureCertificate() { + private SelfSignedCertificate ensureCertificate() { checkState(certificate != null, "certificate not created"); + return certificate; } } diff --git a/junit5/src/main/java/com/linecorp/armeria/internal/testing/ServerRuleDelegate.java b/junit5/src/main/java/com/linecorp/armeria/internal/testing/ServerRuleDelegate.java index 3b6e661d309..f865b68c033 100644 --- a/junit5/src/main/java/com/linecorp/armeria/internal/testing/ServerRuleDelegate.java +++ b/junit5/src/main/java/com/linecorp/armeria/internal/testing/ServerRuleDelegate.java @@ -89,6 +89,7 @@ public void after() { public Server start() { final Server oldServer = server.get(); if (!isStopped(oldServer)) { + assert oldServer != null; return oldServer; } @@ -149,6 +150,7 @@ public Server server() { if (isStopped(server)) { throw new IllegalStateException("server did not start."); } + assert server != null; return server; } @@ -364,7 +366,9 @@ public WebClient webClient() { if (this.webClient.compareAndSet(null, newWebClient)) { return newWebClient; } else { - return this.webClient.get(); + final WebClient oldWebClient = this.webClient.get(); + assert oldWebClient != null; + return oldWebClient; } } @@ -430,7 +434,9 @@ public WebSocketClient webSocketClient() { if (this.webSocketClient.compareAndSet(null, newWebSocketClient)) { return newWebSocketClient; } else { - return this.webSocketClient.get(); + final WebSocketClient oldWebClient = this.webSocketClient.get(); + assert oldWebClient != null; + return oldWebClient; } } diff --git a/kubernetes/src/main/java/com/linecorp/armeria/client/kubernetes/ArmeriaWebSocket.java b/kubernetes/src/main/java/com/linecorp/armeria/client/kubernetes/ArmeriaWebSocket.java index 069bd80b2d8..49b5df31431 100644 --- a/kubernetes/src/main/java/com/linecorp/armeria/client/kubernetes/ArmeriaWebSocket.java +++ b/kubernetes/src/main/java/com/linecorp/armeria/client/kubernetes/ArmeriaWebSocket.java @@ -52,6 +52,7 @@ public boolean send(ByteBuffer buffer) { // 'buffer' may be mutated by the caller, so we need to copy it. final ByteBufAllocator alloc = ClientRequestContext.mapCurrent(RequestContext::alloc, () -> ByteBufAllocator.DEFAULT); + assert alloc != null; final ByteBuf data = alloc.buffer(buffer.remaining()).writeBytes(buffer.duplicate()); final int dataLength = data.readableBytes(); pending.addAndGet(dataLength); diff --git a/logback/logback12/src/main/java/com/linecorp/armeria/common/logback/UnionMap.java b/logback/logback12/src/main/java/com/linecorp/armeria/common/logback/UnionMap.java index 8f3eb47b992..345e11e4bb5 100644 --- a/logback/logback12/src/main/java/com/linecorp/armeria/common/logback/UnionMap.java +++ b/logback/logback12/src/main/java/com/linecorp/armeria/common/logback/UnionMap.java @@ -83,6 +83,7 @@ public boolean containsValue(Object value) { return first.containsValue(value) || second.containsValue(value); } + @Nullable @Override public V get(Object key) { final V value = first.get(key); diff --git a/oauth2/src/main/java/com/linecorp/armeria/client/auth/oauth2/OAuth2ResourceOwnerPasswordCredentialsGrantBuilder.java b/oauth2/src/main/java/com/linecorp/armeria/client/auth/oauth2/OAuth2ResourceOwnerPasswordCredentialsGrantBuilder.java index 2856f8d6238..8c349671145 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/client/auth/oauth2/OAuth2ResourceOwnerPasswordCredentialsGrantBuilder.java +++ b/oauth2/src/main/java/com/linecorp/armeria/client/auth/oauth2/OAuth2ResourceOwnerPasswordCredentialsGrantBuilder.java @@ -65,6 +65,7 @@ public OAuth2ResourceOwnerPasswordCredentialsGrantBuilder userCredentials( * Builds a new instance of {@link OAuth2ResourceOwnerPasswordCredentialsGrant} using configured parameters. */ public OAuth2ResourceOwnerPasswordCredentialsGrant build() { + final Supplier> userCredentialsSupplier = this.userCredentialsSupplier; checkState(userCredentialsSupplier != null, "userCredentialsSupplier must be set."); final ClientAuthentication clientAuthentication = buildClientAuthentication(); final Supplier accessTokenRequestSupplier = () -> { diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/DefaultTokenOperationRequest.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/DefaultTokenOperationRequest.java index ab043478c2f..996bf1e1aae 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/DefaultTokenOperationRequest.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/DefaultTokenOperationRequest.java @@ -53,6 +53,7 @@ public String token() { return token; } + @Nullable @Override public String tokenTypeHint() { return tokenTypeHint; diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidClientException.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidClientException.java index 1a5e73165be..e5af4f04c57 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidClientException.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidClientException.java @@ -44,7 +44,7 @@ public final class InvalidClientException extends TokenRequestException { * thus MUST NOT include characters outside * the set {@code %x21} / {@code %x23-5B} / {@code %x5D-7E}. */ - public InvalidClientException(String errorDescription, @Nullable String errorUri) { + public InvalidClientException(@Nullable String errorDescription, @Nullable String errorUri) { super(errorDescription, errorUri); } @@ -62,7 +62,8 @@ public InvalidClientException(String errorDescription, @Nullable String errorUri * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) */ - public InvalidClientException(String errorDescription, @Nullable String errorUri, Throwable cause) { + public InvalidClientException(@Nullable String errorDescription, @Nullable String errorUri, + @Nullable Throwable cause) { super(errorDescription, errorUri, cause); } } diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidGrantException.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidGrantException.java index 77f9cc704da..eb3bcfc8921 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidGrantException.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidGrantException.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.common.auth.oauth2; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; /** @@ -40,7 +41,7 @@ public final class InvalidGrantException extends TokenRequestException { * thus MUST NOT include characters outside * the set {@code %x21} / {@code %x23-5B} / {@code %x5D-7E}. */ - public InvalidGrantException(String errorDescription, String errorUri) { + public InvalidGrantException(@Nullable String errorDescription, @Nullable String errorUri) { super(errorDescription, errorUri); } @@ -58,7 +59,8 @@ public InvalidGrantException(String errorDescription, String errorUri) { * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) */ - public InvalidGrantException(String errorDescription, String errorUri, Throwable cause) { + public InvalidGrantException(@Nullable String errorDescription, @Nullable String errorUri, + @Nullable Throwable cause) { super(errorDescription, errorUri, cause); } } diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidRequestException.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidRequestException.java index 505df73c72b..ae496906a5e 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidRequestException.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidRequestException.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.common.auth.oauth2; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; /** @@ -40,7 +41,7 @@ public final class InvalidRequestException extends TokenRequestException { * thus MUST NOT include characters outside * the set {@code %x21} / {@code %x23-5B} / {@code %x5D-7E}. */ - public InvalidRequestException(String errorDescription, String errorUri) { + public InvalidRequestException(@Nullable String errorDescription, @Nullable String errorUri) { super(errorDescription, errorUri); } @@ -58,7 +59,8 @@ public InvalidRequestException(String errorDescription, String errorUri) { * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) */ - public InvalidRequestException(String errorDescription, String errorUri, Throwable cause) { + public InvalidRequestException(@Nullable String errorDescription, @Nullable String errorUri, + @Nullable Throwable cause) { super(errorDescription, errorUri, cause); } } diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidScopeException.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidScopeException.java index 32005b38d2a..66436bb3034 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidScopeException.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/InvalidScopeException.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.common.auth.oauth2; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; /** @@ -39,7 +40,7 @@ public final class InvalidScopeException extends TokenRequestException { * thus MUST NOT include characters outside * the set {@code %x21} / {@code %x23-5B} / {@code %x5D-7E}. */ - public InvalidScopeException(String errorDescription, String errorUri) { + public InvalidScopeException(@Nullable String errorDescription, @Nullable String errorUri) { super(errorDescription, errorUri); } @@ -57,7 +58,8 @@ public InvalidScopeException(String errorDescription, String errorUri) { * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) */ - public InvalidScopeException(String errorDescription, String errorUri, Throwable cause) { + public InvalidScopeException(@Nullable String errorDescription, @Nullable String errorUri, + @Nullable Throwable cause) { super(errorDescription, errorUri, cause); } } diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/TokenRequestException.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/TokenRequestException.java index 5dcb4b44fd3..053a171bea5 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/TokenRequestException.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/TokenRequestException.java @@ -100,7 +100,7 @@ public static TokenRequestException parse(String rawResponse) { * thus MUST NOT include characters outside * the set {@code %x21} / {@code %x23-5B} / {@code %x5D-7E}. */ - public TokenRequestException(String errorDescription, @Nullable String errorUri) { + public TokenRequestException(@Nullable String errorDescription, @Nullable String errorUri) { super(errorDescription); this.errorUri = errorUri; } @@ -119,7 +119,8 @@ public TokenRequestException(String errorDescription, @Nullable String errorUri) * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) */ - public TokenRequestException(String errorDescription, @Nullable String errorUri, Throwable cause) { + public TokenRequestException(@Nullable String errorDescription, @Nullable String errorUri, + @Nullable Throwable cause) { super(errorDescription, cause); this.errorUri = errorUri; } diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnauthorizedClientException.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnauthorizedClientException.java index 0f3cfc36319..63498dbcc2c 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnauthorizedClientException.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnauthorizedClientException.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.common.auth.oauth2; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; /** @@ -38,7 +39,7 @@ public final class UnauthorizedClientException extends TokenRequestException { * thus MUST NOT include characters outside * the set {@code %x21} / {@code %x23-5B} / {@code %x5D-7E}. */ - public UnauthorizedClientException(String errorDescription, String errorUri) { + public UnauthorizedClientException(@Nullable String errorDescription, @Nullable String errorUri) { super(errorDescription, errorUri); } @@ -56,7 +57,8 @@ public UnauthorizedClientException(String errorDescription, String errorUri) { * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) */ - public UnauthorizedClientException(String errorDescription, String errorUri, Throwable cause) { + public UnauthorizedClientException(@Nullable String errorDescription, @Nullable String errorUri, + @Nullable Throwable cause) { super(errorDescription, errorUri, cause); } } diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnsupportedGrantTypeException.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnsupportedGrantTypeException.java index 3443e0cf204..20357354e02 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnsupportedGrantTypeException.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnsupportedGrantTypeException.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.common.auth.oauth2; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; /** @@ -39,7 +40,7 @@ public final class UnsupportedGrantTypeException extends TokenRequestException { * thus MUST NOT include characters outside * the set {@code %x21} / {@code %x23-5B} / {@code %x5D-7E}. */ - public UnsupportedGrantTypeException(String errorDescription, String errorUri) { + public UnsupportedGrantTypeException(@Nullable String errorDescription, @Nullable String errorUri) { super(errorDescription, errorUri); } @@ -58,7 +59,8 @@ public UnsupportedGrantTypeException(String errorDescription, String errorUri) { * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) */ - public UnsupportedGrantTypeException(String errorDescription, String errorUri, Throwable cause) { + public UnsupportedGrantTypeException(@Nullable String errorDescription, @Nullable String errorUri, + @Nullable Throwable cause) { super(errorDescription, errorUri, cause); } } diff --git a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnsupportedTokenTypeException.java b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnsupportedTokenTypeException.java index 9cf67c62aa1..721430c20ea 100644 --- a/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnsupportedTokenTypeException.java +++ b/oauth2/src/main/java/com/linecorp/armeria/common/auth/oauth2/UnsupportedTokenTypeException.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.common.auth.oauth2; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; /** @@ -40,7 +41,7 @@ public final class UnsupportedTokenTypeException extends TokenRequestException { * thus MUST NOT include characters outside * the set {@code %x21} / {@code %x23-5B} / {@code %x5D-7E}. */ - public UnsupportedTokenTypeException(String errorDescription, String errorUri) { + public UnsupportedTokenTypeException(@Nullable String errorDescription, @Nullable String errorUri) { super(errorDescription, errorUri); } @@ -59,7 +60,8 @@ public UnsupportedTokenTypeException(String errorDescription, String errorUri) { * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). * (A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.) */ - public UnsupportedTokenTypeException(String errorDescription, String errorUri, Throwable cause) { + public UnsupportedTokenTypeException(@Nullable String errorDescription, @Nullable String errorUri, + @Nullable Throwable cause) { super(errorDescription, errorUri, cause); } } diff --git a/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufRequestConverterFunctionProvider.java b/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufRequestConverterFunctionProvider.java index 010c8978f91..6ee397e7aca 100644 --- a/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufRequestConverterFunctionProvider.java +++ b/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufRequestConverterFunctionProvider.java @@ -24,6 +24,7 @@ import com.google.protobuf.Message; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; import com.linecorp.armeria.server.annotation.RequestConverterFunction; import com.linecorp.armeria.server.annotation.RequestConverterFunctionProvider; @@ -35,6 +36,7 @@ @UnstableApi public final class ProtobufRequestConverterFunctionProvider implements RequestConverterFunctionProvider { + @Nullable @Override public RequestConverterFunction createRequestConverterFunction(Type requestType, RequestConverterFunction requestConverter) { diff --git a/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufResponseConverterFunction.java b/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufResponseConverterFunction.java index 397ecd5fc36..2f36d45dec9 100644 --- a/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufResponseConverterFunction.java +++ b/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufResponseConverterFunction.java @@ -97,6 +97,8 @@ public final class ProtobufResponseConverterFunction implements ResponseConverte // Should never reach here. fromPublisher = null; } + + assert fromPublisher != null; fromPublisherMH = fromPublisher; MethodHandle fromStream; @@ -110,6 +112,8 @@ public final class ProtobufResponseConverterFunction implements ResponseConverte // Should never reach here. fromStream = null; } + + assert fromStream != null; fromStreamMH = fromStream; MethodHandle fromObject; @@ -122,6 +126,8 @@ public final class ProtobufResponseConverterFunction implements ResponseConverte // Should never reach here. fromObject = null; } + + assert fromObject != null; fromObjectMH = fromObject; } @@ -145,6 +151,7 @@ public ProtobufResponseConverterFunction(Printer jsonPrinter) { this.jsonPrinter = requireNonNull(jsonPrinter, "jsonPrinter"); } + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType produceType) { final Class clazz = typeToClass(unwrapUnaryAsyncType(returnType)); @@ -209,6 +216,7 @@ public HttpResponse convertResponse(ServiceRequestContext ctx, ResponseHeaders h if (result instanceof Message) { if (isJson) { + assert contentType != null; final Charset charset = contentType.charset(StandardCharsets.UTF_8); return HttpResponse.of(headers, toJsonHttpData(result, charset), trailers); } @@ -224,6 +232,7 @@ public HttpResponse convertResponse(ServiceRequestContext ctx, ResponseHeaders h } if (isJson) { + assert contentType != null; checkArgument(result != null, "a null value is not allowed for %s", contentType); final Charset charset = contentType.charset(StandardCharsets.UTF_8); diff --git a/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufResponseConverterFunctionProvider.java b/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufResponseConverterFunctionProvider.java index 194fccf6edc..fba79b3ac2e 100644 --- a/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufResponseConverterFunctionProvider.java +++ b/protobuf/src/main/java/com/linecorp/armeria/server/protobuf/ProtobufResponseConverterFunctionProvider.java @@ -27,6 +27,7 @@ import com.google.protobuf.Message; import com.google.protobuf.MessageLite; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; import com.linecorp.armeria.server.annotation.ResponseConverterFunction; import com.linecorp.armeria.server.annotation.ResponseConverterFunctionProvider; @@ -37,6 +38,7 @@ @UnstableApi public final class ProtobufResponseConverterFunctionProvider implements ResponseConverterFunctionProvider { + @Nullable @Override public ResponseConverterFunction createResponseConverterFunction(Type returnType) { if (isSupportedType(returnType)) { diff --git a/resilience4j2/src/main/java/com/linecorp/armeria/resilience4j/circuitbreaker/client/Resilience4JCircuitBreakerClientHandler.java b/resilience4j2/src/main/java/com/linecorp/armeria/resilience4j/circuitbreaker/client/Resilience4JCircuitBreakerClientHandler.java index 38f0558980a..a8c07a13410 100644 --- a/resilience4j2/src/main/java/com/linecorp/armeria/resilience4j/circuitbreaker/client/Resilience4JCircuitBreakerClientHandler.java +++ b/resilience4j2/src/main/java/com/linecorp/armeria/resilience4j/circuitbreaker/client/Resilience4JCircuitBreakerClientHandler.java @@ -27,6 +27,7 @@ import com.linecorp.armeria.client.circuitbreaker.CircuitBreakerMapping; import com.linecorp.armeria.client.circuitbreaker.ClientCircuitBreakerGenerator; import com.linecorp.armeria.common.Request; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; import com.linecorp.armeria.common.circuitbreaker.CircuitBreakerCallback; @@ -102,6 +103,7 @@ public static CircuitBreakerClientHandler of( this.mapping = mapping; } + @Nullable @Override public CircuitBreakerCallback tryRequest(ClientRequestContext ctx, Request req) { final CircuitBreaker circuitBreaker; diff --git a/resteasy/src/main/java/com/linecorp/armeria/client/resteasy/ArmeriaJaxrsClientEngine.java b/resteasy/src/main/java/com/linecorp/armeria/client/resteasy/ArmeriaJaxrsClientEngine.java index 11e304ece09..25c99e58bfb 100644 --- a/resteasy/src/main/java/com/linecorp/armeria/client/resteasy/ArmeriaJaxrsClientEngine.java +++ b/resteasy/src/main/java/com/linecorp/armeria/client/resteasy/ArmeriaJaxrsClientEngine.java @@ -110,6 +110,7 @@ public ArmeriaJaxrsClientEngine(WebClient client, int bufferSize, @Nullable Dura @Override public void close() { + client.options().factory().closeAsync(); } /** diff --git a/resteasy/src/main/java/com/linecorp/armeria/internal/common/resteasy/HttpMessageSubscriberAdapter.java b/resteasy/src/main/java/com/linecorp/armeria/internal/common/resteasy/HttpMessageSubscriberAdapter.java index e4edd1992b4..416ad38ce92 100644 --- a/resteasy/src/main/java/com/linecorp/armeria/internal/common/resteasy/HttpMessageSubscriberAdapter.java +++ b/resteasy/src/main/java/com/linecorp/armeria/internal/common/resteasy/HttpMessageSubscriberAdapter.java @@ -53,6 +53,8 @@ public void onSubscribe(Subscription subscription) { @Override public void onNext(HttpObject httpObject) { + assert subscription != null; + final boolean eos = httpObject.isEndOfStream(); if (httpObject instanceof HttpHeaders) { subscriber.onHeaders((HttpHeaders) httpObject); diff --git a/resteasy/src/test/java/com/linecorp/armeria/server/resteasy/CalculatorServiceClientServerTest.java b/resteasy/src/test/java/com/linecorp/armeria/server/resteasy/CalculatorServiceClientServerTest.java index ec206a9a8d6..30d7273e442 100644 --- a/resteasy/src/test/java/com/linecorp/armeria/server/resteasy/CalculatorServiceClientServerTest.java +++ b/resteasy/src/test/java/com/linecorp/armeria/server/resteasy/CalculatorServiceClientServerTest.java @@ -29,6 +29,7 @@ import org.jboss.resteasy.core.ResteasyDeploymentImpl; import org.jboss.resteasy.spi.ResteasyDeployment; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; @@ -40,6 +41,7 @@ import com.linecorp.armeria.client.WebClientBuilder; import com.linecorp.armeria.client.logging.LoggingClient; import com.linecorp.armeria.client.resteasy.ArmeriaResteasyClientBuilder; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.logging.LogLevel; import com.linecorp.armeria.common.logging.LogWriter; import com.linecorp.armeria.internal.testing.GenerateNativeImageTrace; @@ -70,6 +72,16 @@ protected void configure(ServerBuilder serverBuilder) throws Exception { } }; + @Nullable + private static Client restClient; + + @AfterEach + void tearDown() { + if (restClient != null) { + restClient.close(); + } + } + @Test void testCalcContext() throws Exception { final WebTarget webTarget = newWebTarget(); @@ -121,7 +133,7 @@ private static WebTarget newWebTarget() { .decorator(LoggingClient.builder() .logWriter(logWriter) .newDecorator()); - final Client restClient = ArmeriaResteasyClientBuilder.newBuilder(webClientBuilder).build(); + restClient = ArmeriaResteasyClientBuilder.newBuilder(webClientBuilder).build(); return restClient.target(restServer.httpUri()); } } diff --git a/retrofit2/src/main/java/com/linecorp/armeria/client/retrofit2/ArmeriaCallFactory.java b/retrofit2/src/main/java/com/linecorp/armeria/client/retrofit2/ArmeriaCallFactory.java index 569e9146136..1746e2cf6db 100644 --- a/retrofit2/src/main/java/com/linecorp/armeria/client/retrofit2/ArmeriaCallFactory.java +++ b/retrofit2/src/main/java/com/linecorp/armeria/client/retrofit2/ArmeriaCallFactory.java @@ -171,12 +171,14 @@ public Request request() { return request; } - private synchronized void createRequest() { + private synchronized HttpResponse createRequest() { if (httpResponse != null) { throw new IllegalStateException("executed already"); } executionStateUpdater.compareAndSet(this, ExecutionState.IDLE, ExecutionState.RUNNING); - httpResponse = doCall(callFactory, request); + final HttpResponse newResponse = doCall(callFactory, request); + httpResponse = newResponse; + return newResponse; } @Override @@ -194,8 +196,7 @@ public Response execute() throws IOException { @Override public void enqueue(Callback callback) { - createRequest(); - httpResponse.subscribe(callFactory.subscriberFactory.create(this, callback, request)); + createRequest().subscribe(callFactory.subscriberFactory.create(this, callback, request)); } @Override diff --git a/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextAssembly.java b/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextAssembly.java index a916d657dbb..46224738fc1 100644 --- a/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextAssembly.java +++ b/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextAssembly.java @@ -233,7 +233,10 @@ public static synchronized void disable() { private abstract static class ConditionalOnCurrentRequestContextFunction implements Function { @Override public final T apply(T t) { - return RequestContext.mapCurrent(requestContext -> applyActual(t, requestContext), () -> t); + final T result = RequestContext.mapCurrent(requestContext -> applyActual(t, requestContext), + () -> t); + assert result != null; + return result; } abstract T applyActual(T t, RequestContext ctx); diff --git a/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextCompletableObserver.java b/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextCompletableObserver.java index 60965d9c53c..b826c69611f 100644 --- a/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextCompletableObserver.java +++ b/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextCompletableObserver.java @@ -17,6 +17,7 @@ package com.linecorp.armeria.common.rxjava2; import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.SafeCloseable; import io.reactivex.CompletableObserver; @@ -26,6 +27,7 @@ final class RequestContextCompletableObserver implements CompletableObserver, Disposable { private final CompletableObserver actual; private final RequestContext assemblyContext; + @Nullable private Disposable disposable; RequestContextCompletableObserver(CompletableObserver actual, RequestContext assemblyContext) { @@ -60,11 +62,13 @@ public void onComplete() { @Override public boolean isDisposed() { + assert disposable != null; return disposable.isDisposed(); } @Override public void dispose() { + assert disposable != null; disposable.dispose(); } } diff --git a/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextMaybeObserver.java b/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextMaybeObserver.java index 9682251614b..3014e971df7 100644 --- a/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextMaybeObserver.java +++ b/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextMaybeObserver.java @@ -17,6 +17,7 @@ package com.linecorp.armeria.common.rxjava2; import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.SafeCloseable; import io.reactivex.MaybeObserver; @@ -26,6 +27,7 @@ final class RequestContextMaybeObserver implements MaybeObserver, Disposable { private final MaybeObserver actual; private final RequestContext assemblyContext; + @Nullable private Disposable disposable; RequestContextMaybeObserver(MaybeObserver actual, RequestContext assemblyContext) { @@ -67,11 +69,13 @@ public void onComplete() { @Override public boolean isDisposed() { + assert disposable != null; return disposable.isDisposed(); } @Override public void dispose() { + assert disposable != null; disposable.dispose(); } } diff --git a/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextSingleObserver.java b/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextSingleObserver.java index d29e851441c..d98e3776feb 100644 --- a/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextSingleObserver.java +++ b/rxjava2/src/main/java/com/linecorp/armeria/common/rxjava2/RequestContextSingleObserver.java @@ -17,6 +17,7 @@ package com.linecorp.armeria.common.rxjava2; import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.SafeCloseable; import io.reactivex.SingleObserver; @@ -26,6 +27,7 @@ final class RequestContextSingleObserver implements SingleObserver, Disposable { private final SingleObserver actual; private final RequestContext assemblyContext; + @Nullable private Disposable disposable; RequestContextSingleObserver(SingleObserver actual, RequestContext assemblyContext) { @@ -60,11 +62,13 @@ public void onSuccess(T value) { @Override public boolean isDisposed() { + assert disposable != null; return disposable.isDisposed(); } @Override public void dispose() { + assert disposable != null; disposable.dispose(); } } diff --git a/rxjava2/src/main/java/com/linecorp/armeria/server/rxjava2/ObservableResponseConverterFunction.java b/rxjava2/src/main/java/com/linecorp/armeria/server/rxjava2/ObservableResponseConverterFunction.java index ac380f28a4b..780a77eb8d6 100644 --- a/rxjava2/src/main/java/com/linecorp/armeria/server/rxjava2/ObservableResponseConverterFunction.java +++ b/rxjava2/src/main/java/com/linecorp/armeria/server/rxjava2/ObservableResponseConverterFunction.java @@ -79,6 +79,7 @@ public ObservableResponseConverterFunction(ResponseConverterFunction responseCon exceptionHandler = null; } + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType produceType) { final Class clazz = typeToClass(unwrapUnaryAsyncType(returnType)); diff --git a/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextAssembly.java b/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextAssembly.java index a4c0a384a43..92799f8617e 100644 --- a/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextAssembly.java +++ b/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextAssembly.java @@ -222,7 +222,10 @@ private RequestContextAssembly() {} private abstract static class ConditionalOnCurrentRequestContextFunction implements Function { @Override public final T apply(T t) { - return RequestContext.mapCurrent(requestContext -> applyActual(t, requestContext), () -> t); + final T result = RequestContext.mapCurrent(requestContext -> applyActual(t, requestContext), + () -> t); + assert result != null; + return result; } abstract T applyActual(T t, RequestContext ctx); diff --git a/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextCompletableObserver.java b/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextCompletableObserver.java index 15b6cf9324c..09c0219f1c5 100644 --- a/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextCompletableObserver.java +++ b/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextCompletableObserver.java @@ -17,6 +17,7 @@ package com.linecorp.armeria.common.rxjava3; import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.SafeCloseable; import io.reactivex.rxjava3.core.CompletableObserver; @@ -26,6 +27,7 @@ final class RequestContextCompletableObserver implements CompletableObserver, Disposable { private final CompletableObserver actual; private final RequestContext assemblyContext; + @Nullable private Disposable disposable; RequestContextCompletableObserver(CompletableObserver actual, RequestContext assemblyContext) { @@ -60,11 +62,13 @@ public void onComplete() { @Override public boolean isDisposed() { + assert disposable != null; return disposable.isDisposed(); } @Override public void dispose() { + assert disposable != null; disposable.dispose(); } } diff --git a/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextMaybeObserver.java b/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextMaybeObserver.java index 62e0536f4a2..2edfa55d1b7 100644 --- a/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextMaybeObserver.java +++ b/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextMaybeObserver.java @@ -17,6 +17,7 @@ package com.linecorp.armeria.common.rxjava3; import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.SafeCloseable; import io.reactivex.rxjava3.core.MaybeObserver; @@ -26,6 +27,7 @@ final class RequestContextMaybeObserver implements MaybeObserver, Disposable { private final MaybeObserver actual; private final RequestContext assemblyContext; + @Nullable private Disposable disposable; RequestContextMaybeObserver(MaybeObserver actual, RequestContext assemblyContext) { @@ -67,11 +69,13 @@ public void onComplete() { @Override public boolean isDisposed() { + assert disposable != null; return disposable.isDisposed(); } @Override public void dispose() { + assert disposable != null; disposable.dispose(); } } diff --git a/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextSingleObserver.java b/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextSingleObserver.java index c7bf7b8416d..6bf9b3bd0ef 100644 --- a/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextSingleObserver.java +++ b/rxjava3/src/main/java/com/linecorp/armeria/common/rxjava3/RequestContextSingleObserver.java @@ -17,6 +17,7 @@ package com.linecorp.armeria.common.rxjava3; import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.util.SafeCloseable; import io.reactivex.rxjava3.core.SingleObserver; @@ -26,6 +27,7 @@ final class RequestContextSingleObserver implements SingleObserver, Disposable { private final SingleObserver actual; private final RequestContext assemblyContext; + @Nullable private Disposable disposable; RequestContextSingleObserver(SingleObserver actual, RequestContext assemblyContext) { @@ -60,11 +62,13 @@ public void onSuccess(T value) { @Override public boolean isDisposed() { + assert disposable != null; return disposable.isDisposed(); } @Override public void dispose() { + assert disposable != null; disposable.dispose(); } } diff --git a/rxjava3/src/main/java/com/linecorp/armeria/server/rxjava3/ObservableResponseConverterFunction.java b/rxjava3/src/main/java/com/linecorp/armeria/server/rxjava3/ObservableResponseConverterFunction.java index 5377c0fd97e..c01f88c8692 100644 --- a/rxjava3/src/main/java/com/linecorp/armeria/server/rxjava3/ObservableResponseConverterFunction.java +++ b/rxjava3/src/main/java/com/linecorp/armeria/server/rxjava3/ObservableResponseConverterFunction.java @@ -79,6 +79,7 @@ public ObservableResponseConverterFunction(ResponseConverterFunction responseCon exceptionHandler = null; } + @Nullable @Override public Boolean isResponseStreaming(Type returnType, @Nullable MediaType produceType) { final Class clazz = typeToClass(unwrapUnaryAsyncType(returnType)); diff --git a/saml/src/main/java/com/linecorp/armeria/server/saml/SamlDecorator.java b/saml/src/main/java/com/linecorp/armeria/server/saml/SamlDecorator.java index 71de8d07548..e02baf91bb4 100644 --- a/saml/src/main/java/com/linecorp/armeria/server/saml/SamlDecorator.java +++ b/saml/src/main/java/com/linecorp/armeria/server/saml/SamlDecorator.java @@ -198,6 +198,7 @@ private AuthnRequest createAuthRequest(SamlIdentityProviderConfig idp, String de // The ProtocolBinding attribute is mutually exclusive with the AssertionConsumerServiceIndex attribute // and is typically accompanied by the AssertionConsumerServiceURL attribute. final SamlPortConfig portConfig = portConfigHolder.config(); + assert portConfig != null; final SamlEndpoint acsEndpoint = idp.acsEndpoint() != null ? idp.acsEndpoint() : sp.defaultAcsConfig().endpoint(); authnRequest.setAssertionConsumerServiceURL(acsEndpoint.toUriString(portConfig.scheme().uriText(), diff --git a/saml/src/main/java/com/linecorp/armeria/server/saml/SamlIdentityProviderConfigBuilder.java b/saml/src/main/java/com/linecorp/armeria/server/saml/SamlIdentityProviderConfigBuilder.java index cc0f9b7d74e..43b36adfa90 100644 --- a/saml/src/main/java/com/linecorp/armeria/server/saml/SamlIdentityProviderConfigBuilder.java +++ b/saml/src/main/java/com/linecorp/armeria/server/saml/SamlIdentityProviderConfigBuilder.java @@ -161,11 +161,14 @@ public SamlServiceProviderBuilder and() { * Builds a {@link SamlIdentityProviderConfig}. */ SamlIdentityProviderConfig build(CredentialResolverAdapter credentialResolver) { - checkState(entityId != null, "entity ID of the identity provider is not set"); + checkState(entityId != null, "entity ID of the identity provider is not set."); + checkState(ssoEndpoint != null, "SSO endpoint is not set."); // Use the entityId as a default key name. final Credential signing = credentialResolver.apply(firstNonNull(signingKey, entityId)); + checkState(signing != null, "CredentialResolver.apply() returned null for a signing key."); final Credential encryption = credentialResolver.apply(firstNonNull(encryptionKey, entityId)); + checkState(encryption != null, "CredentialResolver.apply() returned null for an encryption key."); return new SamlIdentityProviderConfig(entityId, signing, diff --git a/saml/src/main/java/com/linecorp/armeria/server/saml/SamlService.java b/saml/src/main/java/com/linecorp/armeria/server/saml/SamlService.java index 0cae39487a8..f9ab8f3b44d 100644 --- a/saml/src/main/java/com/linecorp/armeria/server/saml/SamlService.java +++ b/saml/src/main/java/com/linecorp/armeria/server/saml/SamlService.java @@ -169,6 +169,8 @@ public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exc } final SamlPortConfig portConfig = portConfigHolder.config(); + assert portConfig != null; + final boolean isTls = ctx.sessionProtocol().isTls(); if (portConfig.scheme().isTls() != isTls) { if (isTls) { diff --git a/settings.gradle b/settings.gradle index 72a889f1cfb..9fa62a85a16 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ plugins { // automatically download one based on the foojay Disco API. // https://docs.gradle.org/8.1.1/userguide/toolchains.html#sec:provisioning id 'org.gradle.toolchains.foojay-resolver-convention' version '0.6.0' - id 'com.gradle.develocity' version '3.17.5' + id 'com.gradle.develocity' version '3.17.6' // adds additional metadata to build scans id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.0.2' } @@ -182,6 +182,10 @@ includeWithFlags ':thrift0.17', 'java', 'publish', 'rel project(':thrift0.17').projectDir = file('thrift/thrift0.17') includeWithFlags ':thrift0.18', 'java11', 'publish', 'relocate', 'no_aggregation', 'native' project(':thrift0.18').projectDir = file('thrift/thrift0.18') +includeWithFlags ':thrift0.19', 'java11', 'publish', 'relocate', 'no_aggregation', 'native' +project(':thrift0.19').projectDir = file('thrift/thrift0.19') +includeWithFlags ':thrift0.20', 'java11', 'publish', 'relocate', 'no_aggregation', 'native' +project(':thrift0.20').projectDir = file('thrift/thrift0.20') includeWithFlags ':tomcat8', 'java', 'publish', 'relocate', 'no_aggregation' includeWithFlags ':tomcat9', 'java', 'publish', 'relocate', 'no_aggregation' includeWithFlags ':tomcat10', 'java11', 'publish', 'relocate' diff --git a/site/build.gradle b/site/build.gradle index be0691b50e6..7f07ae79072 100644 --- a/site/build.gradle +++ b/site/build.gradle @@ -23,15 +23,10 @@ plugins { } node { - version = '16.19.1' + version = '22.3.0' + npmVersion = '10.8.1' download = true npmInstallCommand = "ci" - - // Change the cache location under Gradle user home directory so that it's cached by CI. - if (System.getenv('CI') != null) { - workDir = file("${gradle.gradleUserHomeDir}/caches/nodejs/${project.name}") - npmWorkDir = file("${gradle.gradleUserHomeDir}/caches/npm/${project.name}") - } } // Add the option that works around the dependency conflicts. diff --git a/site/src/components/emoji.tsx b/site/src/components/emoji.tsx index 2fe5e082cb3..226b4cbca63 100644 --- a/site/src/components/emoji.tsx +++ b/site/src/components/emoji.tsx @@ -12,7 +12,7 @@ function svgEmoji(input: string) { return emoji(input, { // baseUrl shouldn't end with '/'. // https://github.com/appfigures/react-easy-emoji/issues/25 - baseUrl: 'https://twemoji.maxcdn.com/2/svg', + baseUrl: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg', ext: '.svg', size: '', }); diff --git a/site/src/pages/index.tsx b/site/src/pages/index.tsx index 18c97f11a2d..0860f409f98 100644 --- a/site/src/pages/index.tsx +++ b/site/src/pages/index.tsx @@ -114,8 +114,8 @@ const IndexPage = (props: RouteComponentProps): JSX.Element => { Netty {' '} and his colleagues at{' '} - - + + LY Corporation {' '} diff --git a/site/src/pages/release-notes/1.29.3.mdx b/site/src/pages/release-notes/1.29.3.mdx new file mode 100644 index 00000000000..7ff96b07fbb --- /dev/null +++ b/site/src/pages/release-notes/1.29.3.mdx @@ -0,0 +1,28 @@ +--- +date: 2024-07-26 +--- + +## 🛠️ Bug fixes + +- `NullPointerException` is no longer raised when handles errors. #5815 #5816 +- Fixed a regression where a protocol violation error is not handled + by #5811 +- Fixed compatibility issue with Java 9 modules by excluding `java.security.Provider` SPI configuration from + the shadowed JAR. #5825 +- Reapplied changes that were unexpectedly overwritten for Java module support. #5835 +- now correctly returns `RESOURCE_EXHAUSTED` status when a response message exceeds the + maximum allowed length. #5818 #5824 +- Relaxed constraints on gRPC JSON transcoding to log a warning instead of raising an `IllegalArgumentException` + when used with non-unary gRPC methods. #5828 #5829 + +## 🙇 Thank you + + diff --git a/site/src/pages/release-notes/1.29.4.mdx b/site/src/pages/release-notes/1.29.4.mdx new file mode 100644 index 00000000000..1ff67c8476e --- /dev/null +++ b/site/src/pages/release-notes/1.29.4.mdx @@ -0,0 +1,18 @@ +--- +date: 2024-07-30 +--- + +## 🛠️ Bug fixes + +- An annotated service that uses a annotation with the specified value now works correctly. #5841 + - This is a regression introduced by #5547, affecting 1.29.0 through 1.29.3. + +## 🙇 Thank you + + \ No newline at end of file diff --git a/spring/boot3-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/ArmeriaSpringActuatorAutoConfiguration.java b/spring/boot3-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/ArmeriaSpringActuatorAutoConfiguration.java index 73661095477..3e7aa5f17c2 100644 --- a/spring/boot3-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/ArmeriaSpringActuatorAutoConfiguration.java +++ b/spring/boot3-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/ArmeriaSpringActuatorAutoConfiguration.java @@ -323,6 +323,7 @@ private static Integer obtainManagementServerPort(ServerBuilder serverBuilder, ManagementServerProperties properties) { Object internalServices = null; if (MANAGEMENT_SERVER_PORT_METHOD != null) { + assert INTERNAL_SERVICES_CLASS != null; internalServices = findBean(beanFactory, INTERNAL_SERVICES_CLASS); } @@ -338,6 +339,7 @@ private static Integer obtainManagementServerPort(ServerBuilder serverBuilder, return managementPort.getPort(); } + assert MANAGEMENT_SERVER_PORT_METHOD != null; try { final Port port = (Port) MANAGEMENT_SERVER_PORT_METHOD.invoke(internalServices); if (port == null) { @@ -352,6 +354,10 @@ private static Integer obtainManagementServerPort(ServerBuilder serverBuilder, @Nullable private static Integer getExposedInternalServicePort(BeanFactory beanFactory, ArmeriaSettings armeriaSettings) { + if (INTERNAL_SERVICES_CLASS == null) { + return null; + } + final Object internalServices = findBean(beanFactory, INTERNAL_SERVICES_CLASS); if (internalServices == null) { return null; @@ -365,6 +371,8 @@ private static Integer getExposedInternalServicePort(BeanFactory beanFactory, if (!actuatorEnabled) { return null; } + + assert internalServiceProperties != null; return internalServiceProperties.getPort(); } @@ -381,7 +389,7 @@ private static T findBean(BeanFactory beanFactory, Class clazz) { private static void addLocalManagementPortPropertyAlias(ConfigurableEnvironment environment, Integer port) { environment.getPropertySources().addLast(new PropertySource("Management Server") { - + @Nullable @Override public Object getProperty(String name) { if ("local.management.port".equals(name)) { diff --git a/spring/boot3-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/CorsEndpointProperties.java b/spring/boot3-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/CorsEndpointProperties.java index d77908339de..d23b4d3b0c1 100644 --- a/spring/boot3-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/CorsEndpointProperties.java +++ b/spring/boot3-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/CorsEndpointProperties.java @@ -39,6 +39,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.DurationUnit; +import com.linecorp.armeria.common.annotation.Nullable; + /** * Fork of writeSubscriber) { synchronized (this) { - Assert.state(this.writeSubscriber == null, "Only one write subscriber supported"); + checkState(this.writeSubscriber == null, "Only one write subscriber supported"); this.writeSubscriber = writeSubscriber; if (error != null || (completed && item == null)) { this.writeSubscriber.onSubscribe(Operators.emptySubscription()); diff --git a/testing-internal/src/main/java/com/linecorp/armeria/internal/testing/webapp/WebAppContainerMutualTlsTest.java b/testing-internal/src/main/java/com/linecorp/armeria/internal/testing/webapp/WebAppContainerMutualTlsTest.java index 9097f5ea454..db1d50f1ec0 100644 --- a/testing-internal/src/main/java/com/linecorp/armeria/internal/testing/webapp/WebAppContainerMutualTlsTest.java +++ b/testing-internal/src/main/java/com/linecorp/armeria/internal/testing/webapp/WebAppContainerMutualTlsTest.java @@ -86,6 +86,7 @@ public void mutualTlsAttrs(SessionProtocol sessionProtocol) throws Exception { final AggregatedHttpResponse res = client.get("/jsp/mutual_tls.jsp").aggregate().join(); final SSLSession sslSession = server().requestContextCaptor().take().sslSession(); + assert sslSession != null; final String expectedId; if (sslSession.getId() != null) { expectedId = BaseEncoding.base16().encode(sslSession.getId()); diff --git a/testing-internal/src/main/java/com/linecorp/armeria/internal/testing/webapp/WebAppContainerTest.java b/testing-internal/src/main/java/com/linecorp/armeria/internal/testing/webapp/WebAppContainerTest.java index 31ad581316b..1eb21b47876 100644 --- a/testing-internal/src/main/java/com/linecorp/armeria/internal/testing/webapp/WebAppContainerTest.java +++ b/testing-internal/src/main/java/com/linecorp/armeria/internal/testing/webapp/WebAppContainerTest.java @@ -131,7 +131,7 @@ public void japanesePath() throws Exception { final AggregatedHttpResponse response = server().blockingWebClient().get( "/jsp/" + URLEncoder.encode("日本語", "UTF-8") + "/index.jsp"); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); final String actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).isEqualTo( @@ -149,7 +149,7 @@ public void getWithQueryString() throws Exception { AggregatedHttpResponse response = server().blockingWebClient().get( "/jsp/query_string.jsp?foo=%31&bar=%32"); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); String actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).isEqualTo( @@ -160,7 +160,7 @@ public void getWithQueryString() throws Exception { response = server().blockingWebClient().get( "/jsp/query_string.jsp?foo=%33&bar=%34"); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).isEqualTo( @@ -176,7 +176,7 @@ public void postWithQueryString() throws Exception { HttpMethod.POST, "jsp/query_string.jsp?foo=3").contentType(MediaType.FORM_DATA).build(); AggregatedHttpResponse response = server().blockingWebClient().execute(headers, "bar=4"); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); String actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).isEqualTo( @@ -188,7 +188,7 @@ public void postWithQueryString() throws Exception { HttpMethod.POST, "jsp/query_string.jsp?foo=5").contentType(MediaType.FORM_DATA).build(); response = server().blockingWebClient().execute(headers, "bar=6"); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).isEqualTo( @@ -204,7 +204,7 @@ public void echoPost() throws Exception { server().blockingWebClient(cb -> cb.responseTimeoutMillis(0)) .post("/jsp/echo_post.jsp", HttpData.ofUtf8("test")); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); final String actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).isEqualTo( @@ -219,7 +219,7 @@ public void echoPostWithEmptyBody() throws Exception { final AggregatedHttpResponse response = server().blockingWebClient().post( "/jsp/echo_post.jsp", HttpData.empty()); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); final String actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).isEqualTo( @@ -234,7 +234,7 @@ public void addressesAndPorts_127001() throws Exception { final AggregatedHttpResponse response = WebClient.of(server().httpUri()).blocking() .get("/jsp/addrs_and_ports.jsp"); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); final String actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).matches( @@ -256,7 +256,7 @@ public void addressesAndPorts_localhost() throws Exception { "localhost:1111"); final AggregatedHttpResponse response = WebClient.of(server().httpUri()).blocking().execute(headers); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/html"); + assertThat(String.valueOf(response.contentType())).startsWith("text/html"); final String actualContent = CR_OR_LF.matcher(response.contentUtf8()) .replaceAll(""); assertThat(actualContent).matches( @@ -285,7 +285,7 @@ public void largeResponse() throws Exception { protected void testLarge(String path, boolean requiresContentLength) throws IOException { final AggregatedHttpResponse response = server().blockingWebClient().get(path); assertThat(response.status()).isSameAs(HttpStatus.OK); - assertThat(response.contentType().toString()).startsWith("text/plain"); + assertThat(String.valueOf(response.contentType())).startsWith("text/plain"); if (requiresContentLength) { // Check if the content-length header matches. assertThat(response.headers().contentLength()).isEqualTo(response.content().length()); @@ -307,6 +307,7 @@ public void tlsAttrs(SessionProtocol sessionProtocol) throws Exception { final AggregatedHttpResponse res = client.get("/jsp/tls.jsp").aggregate().join(); final SSLSession sslSession = server().requestContextCaptor().take().sslSession(); + assert sslSession != null; final String expectedId; if (sslSession.getId() != null) { expectedId = BaseEncoding.base16().encode(sslSession.getId()); diff --git a/thrift/thrift0.12/src/test/thrift/TTextProtocolTest.thrift b/thrift/thrift0.12/src/test/thrift/TTextProtocolTest.thrift index fcd6d1b62d3..a3d9761b0d1 100644 --- a/thrift/thrift0.12/src/test/thrift/TTextProtocolTest.thrift +++ b/thrift/thrift0.12/src/test/thrift/TTextProtocolTest.thrift @@ -104,7 +104,7 @@ struct TTextProtocolTestMsg { 22: required list x; - 23: Moji y; // Does not support string values. + 23: Moji y; // Does not support string values for thrift version < 0.19. 27: required map aa; diff --git a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/common/thrift/ThriftReply.java b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/common/thrift/ThriftReply.java index e55558fa04c..d6bb1eae583 100644 --- a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/common/thrift/ThriftReply.java +++ b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/common/thrift/ThriftReply.java @@ -88,6 +88,7 @@ public boolean isException() { if (isException()) { throw new IllegalStateException("not a reply but an exception"); } + assert result != null; return result; } @@ -100,6 +101,7 @@ public TApplicationException exception() { if (!isException()) { throw new IllegalStateException("not an exception but a reply"); } + assert exception != null; return exception; } diff --git a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/common/thrift/text/TTextProtocol.java b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/common/thrift/text/TTextProtocol.java index aefc75b8dbf..f20ca6ab13e 100644 --- a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/common/thrift/text/TTextProtocol.java +++ b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/common/thrift/text/TTextProtocol.java @@ -442,6 +442,7 @@ public TMessage readMessageBegin() throws TException { } catch (IOException e) { throw new TException("Could not parse input, is it valid json?", e); } + assert root != null; if (!root.isObject()) { throw new TException("The top level of the input must be a json object with method and args!"); } diff --git a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/common/thrift/DefaultThriftProtocolFactoryProvider.java b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/common/thrift/DefaultThriftProtocolFactoryProvider.java index 136d28e9b7d..18b845a5dea 100644 --- a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/common/thrift/DefaultThriftProtocolFactoryProvider.java +++ b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/common/thrift/DefaultThriftProtocolFactoryProvider.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableSet; import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.thrift.ThriftProtocolFactories; import com.linecorp.armeria.common.thrift.ThriftProtocolFactoryProvider; import com.linecorp.armeria.common.thrift.ThriftSerializationFormats; @@ -46,6 +47,7 @@ protected Set serializationFormats() { return SERIALIZATION_FORMATS; } + @Nullable @Override protected TProtocolFactory protocolFactory(SerializationFormat serializationFormat, int maxStringLength, int maxContainerLength) { diff --git a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServicePlugin.java b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServicePlugin.java index 4679254d179..8a06435d717 100644 --- a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServicePlugin.java +++ b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServicePlugin.java @@ -365,6 +365,7 @@ public Set> supportedExampleRequestTypes() { return ImmutableSet.of(TBase.class); } + @Nullable @Override public String guessServiceName(Object exampleRequest) { final TBase exampleTBase = asTBase(exampleRequest); @@ -375,6 +376,7 @@ public String guessServiceName(Object exampleRequest) { return exampleTBase.getClass().getEnclosingClass().getName(); } + @Nullable @Override public String guessServiceMethodName(Object exampleRequest) { final TBase exampleTBase = asTBase(exampleRequest); @@ -387,6 +389,7 @@ public String guessServiceMethodName(Object exampleRequest) { typeName.length() - REQUEST_STRUCT_SUFFIX.length()); } + @Nullable @Override public String serializeExampleRequest(String serviceName, String methodName, Object exampleRequest) { diff --git a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocStringExtractor.java b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocStringExtractor.java index fd6356b1d21..6b6374a0f9c 100644 --- a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocStringExtractor.java +++ b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocStringExtractor.java @@ -65,6 +65,7 @@ protected Map getDocStringsFromFiles(Map files) final Map namespaces = (Map) json.getOrDefault("namespaces", ImmutableMap.of()); final String packageName = (String) namespaces.get("java"); + assert packageName != null : "Missing namespace for Java? " + namespaces; json.forEach((key, children) -> { if (children instanceof Collection) { @SuppressWarnings("unchecked") diff --git a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/server/thrift/THttpService.java b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/server/thrift/THttpService.java index 6f3fd41be10..a41eaa945ae 100644 --- a/thrift/thrift0.13/src/main/java/com/linecorp/armeria/server/thrift/THttpService.java +++ b/thrift/thrift0.13/src/main/java/com/linecorp/armeria/server/thrift/THttpService.java @@ -16,6 +16,7 @@ package com.linecorp.armeria.server.thrift; +import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static java.util.Objects.requireNonNull; @@ -504,8 +505,9 @@ private void decodeAndInvoke( try (HttpData content = req.content()) { final ByteBuf buf = content.byteBuf(); final TByteBufTransport inTransport = new TByteBufTransport(buf); - final TProtocol inProto = requestProtocolFactories.get(serializationFormat) - .getProtocol(inTransport); + final TProtocolFactory protocolFactory = requestProtocolFactories.get(serializationFormat); + assert protocolFactory != null; + final TProtocol inProto = protocolFactory.getProtocol(inTransport); final TMessage header; final TBase args; @@ -525,7 +527,7 @@ private void decodeAndInvoke( if (e instanceof TProtocolException && ((TProtocolException) e).getType() == TProtocolException.SIZE_LIMIT) { httpStatus = HttpStatus.REQUEST_ENTITY_TOO_LARGE; - message = e.getMessage(); + message = firstNonNull(e.getMessage(), httpStatus.toString()); } else { httpStatus = HttpStatus.BAD_REQUEST; message = "Failed to decode a " + serializationFormat + " header"; @@ -774,8 +776,9 @@ private HttpData encodeSuccess(ServiceRequestContext ctx, RpcResponse reply, boolean success = false; try { final TTransport transport = new TByteBufTransport(buf); - final TProtocol outProto = responseProtocolFactories.get(serializationFormat) - .getProtocol(transport); + final TProtocolFactory protocolFactory = responseProtocolFactories.get(serializationFormat); + assert protocolFactory != null; + final TProtocol outProto = protocolFactory.getProtocol(transport); final TMessage header = new TMessage(methodName, TMessageType.REPLY, seqId); outProto.writeMessageBegin(header); result.write(outProto); @@ -822,8 +825,9 @@ private HttpData encodeException(ServiceRequestContext ctx, boolean success = false; try { final TTransport transport = new TByteBufTransport(buf); - final TProtocol outProto = responseProtocolFactories.get(serializationFormat) - .getProtocol(transport); + final TProtocolFactory protocolFactory = responseProtocolFactories.get(serializationFormat); + assert protocolFactory != null; + final TProtocol outProto = protocolFactory.getProtocol(transport); final TMessage header = new TMessage(methodName, TMessageType.EXCEPTION, seqId); outProto.writeMessageBegin(header); appException.write(outProto); diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/client/thrift/ServletTestUtils.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/client/thrift/ServletTestUtils.java new file mode 100644 index 00000000000..7d0a35043e0 --- /dev/null +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/client/thrift/ServletTestUtils.java @@ -0,0 +1,155 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.client.thrift; + +import static org.assertj.core.api.Assertions.fail; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.thrift.server.TServlet; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +import com.linecorp.armeria.common.thrift.ThriftProtocolFactories; + +import testing.thrift.main.HelloService.Processor; + +final class ServletTestUtils { + + private ServletTestUtils() {} + + static final String TSERVLET_PATH = "/thrift"; + + @SuppressWarnings("unchecked") + private static final Servlet thriftServlet = + new TServlet(new Processor(name -> "Hello, " + name + '!'), ThriftProtocolFactories.binary(0, 0)); + + private static final Servlet rootServlet = new HttpServlet() { + private static final long serialVersionUID = 6765028749367036441L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + resp.setStatus(404); + resp.setContentLength(0); + } + }; + + static Server startHttp1(AtomicBoolean sendConnectionClose) throws Exception { + final Server server = new Server(0); + + final ServletHandler handler = new ServletHandler(); + handler.addServletWithMapping(newServletHolder(thriftServlet), TSERVLET_PATH); + handler.addServletWithMapping(newServletHolder(rootServlet), "/"); + handler.addFilterWithMapping(new FilterHolder(new ConnectionCloseFilter(sendConnectionClose)), "/*", + EnumSet.of(DispatcherType.REQUEST)); + + server.setHandler(handler); + + for (Connector c : server.getConnectors()) { + for (ConnectionFactory f : c.getConnectionFactories()) { + for (String p : f.getProtocols()) { + if (p.startsWith("h2c")) { + fail("Attempted to create a Jetty server without HTTP/2 support, but failed: " + + f.getProtocols()); + } + } + } + } + + server.start(); + return server; + } + + static Server startHttp2() throws Exception { + final Server server = new Server(); + // Common HTTP configuration. + final HttpConfiguration config = new HttpConfiguration(); + + // HTTP/1.1 support. + final HttpConnectionFactory http1 = new HttpConnectionFactory(config); + + // HTTP/2 cleartext support. + final HTTP2CServerConnectionFactory http2c = new HTTP2CServerConnectionFactory(config); + + // Add the connector. + final ServerConnector connector = new ServerConnector(server, http1, http2c); + connector.setPort(0); + server.addConnector(connector); + + // Add the servlet. + final ServletHandler handler = new ServletHandler(); + handler.addServletWithMapping(newServletHolder(thriftServlet), TSERVLET_PATH); + server.setHandler(handler); + + // Start the server. + server.start(); + return server; + } + + private static ServletHolder newServletHolder(Servlet rootServlet) { + final ServletHolder holder = new ServletHolder(rootServlet); + holder.setInitParameter("cacheControl", "max-age=0, public"); + return holder; + } + + private static class ConnectionCloseFilter implements Filter { + private final AtomicBoolean sendConnectionClose; + + ConnectionCloseFilter(AtomicBoolean sendConnectionClose) { + this.sendConnectionClose = sendConnectionClose; + } + + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + chain.doFilter(request, response); + + if (sendConnectionClose.get()) { + ((HttpServletResponse) response).setHeader("Connection", "close"); + } + } + + @Override + public void destroy() {} + } +} diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/client/thrift/ThriftOverHttpClientTServletIntegrationTest.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/client/thrift/ThriftOverHttpClientTServletIntegrationTest.java index 1e982bf5ed6..06d9b8c9049 100644 --- a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/client/thrift/ThriftOverHttpClientTServletIntegrationTest.java +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/client/thrift/ThriftOverHttpClientTServletIntegrationTest.java @@ -15,45 +15,23 @@ */ package com.linecorp.armeria.client.thrift; +import static com.linecorp.armeria.client.thrift.ServletTestUtils.TSERVLET_PATH; +import static com.linecorp.armeria.client.thrift.ServletTestUtils.startHttp1; +import static com.linecorp.armeria.client.thrift.ServletTestUtils.startHttp2; import static com.linecorp.armeria.common.SessionProtocol.H1C; import static com.linecorp.armeria.common.SessionProtocol.H2C; import static com.linecorp.armeria.common.SessionProtocol.HTTP; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import java.io.IOException; import java.net.InetSocketAddress; -import java.util.EnumSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; -import javax.servlet.DispatcherType; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.Servlet; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.apache.thrift.server.TServlet; -import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -67,10 +45,8 @@ import com.linecorp.armeria.common.ClosedSessionException; import com.linecorp.armeria.common.SessionProtocol; import com.linecorp.armeria.common.logging.RequestLogProperty; -import com.linecorp.armeria.common.thrift.ThriftProtocolFactories; import testing.thrift.main.HelloService; -import testing.thrift.main.HelloService.Processor; /** * Test to verify interaction between armeria client and official thrift @@ -83,22 +59,6 @@ class ThriftOverHttpClientTServletIntegrationTest { private static final int MAX_RETRIES = 9; - private static final String TSERVLET_PATH = "/thrift"; - - @SuppressWarnings("unchecked") - private static final Servlet thriftServlet = - new TServlet(new Processor(name -> "Hello, " + name + '!'), ThriftProtocolFactories.binary(0, 0)); - - private static final Servlet rootServlet = new HttpServlet() { - private static final long serialVersionUID = 6765028749367036441L; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) { - resp.setStatus(404); - resp.setContentLength(0); - } - }; - private static final AtomicBoolean sendConnectionClose = new AtomicBoolean(); private static Server http1server; @@ -106,69 +66,10 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) { @BeforeAll static void createServer() throws Exception { - http1server = startHttp1(); + http1server = startHttp1(sendConnectionClose); http2server = startHttp2(); } - private static Server startHttp1() throws Exception { - final Server server = new Server(0); - - final ServletHandler handler = new ServletHandler(); - handler.addServletWithMapping(newServletHolder(thriftServlet), TSERVLET_PATH); - handler.addServletWithMapping(newServletHolder(rootServlet), "/"); - handler.addFilterWithMapping(new FilterHolder(new ConnectionCloseFilter()), "/*", - EnumSet.of(DispatcherType.REQUEST)); - - server.setHandler(handler); - - for (Connector c : server.getConnectors()) { - for (ConnectionFactory f : c.getConnectionFactories()) { - for (String p : f.getProtocols()) { - if (p.startsWith("h2c")) { - fail("Attempted to create a Jetty server without HTTP/2 support, but failed: " + - f.getProtocols()); - } - } - } - } - - server.start(); - return server; - } - - @Nonnull - private static ServletHolder newServletHolder(Servlet rootServlet) { - final ServletHolder holder = new ServletHolder(rootServlet); - holder.setInitParameter("cacheControl", "max-age=0, public"); - return holder; - } - - private static Server startHttp2() throws Exception { - final Server server = new Server(); - // Common HTTP configuration. - final HttpConfiguration config = new HttpConfiguration(); - - // HTTP/1.1 support. - final HttpConnectionFactory http1 = new HttpConnectionFactory(config); - - // HTTP/2 cleartext support. - final HTTP2CServerConnectionFactory http2c = new HTTP2CServerConnectionFactory(config); - - // Add the connector. - final ServerConnector connector = new ServerConnector(server, http1, http2c); - connector.setPort(0); - server.addConnector(connector); - - // Add the servlet. - final ServletHandler handler = new ServletHandler(); - handler.addServletWithMapping(newServletHolder(thriftServlet), TSERVLET_PATH); - server.setHandler(handler); - - // Start the server. - server.start(); - return server; - } - @AfterAll static void destroyServer() throws Exception { CompletableFuture.runAsync(() -> { @@ -322,23 +223,4 @@ private static String http2uri(SessionProtocol protocol) { private static int http2Port() { return ((NetworkConnector) http2server.getConnectors()[0]).getLocalPort(); } - - private static class ConnectionCloseFilter implements Filter { - @Override - public void init(FilterConfig filterConfig) {} - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - chain.doFilter(request, response); - - if (sendConnectionClose.get()) { - ((HttpServletResponse) response).setHeader("Connection", "close"); - } - } - - @Override - public void destroy() {} - } } diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/common/thrift/text/TTextProtocolTest.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/common/thrift/text/TTextProtocolTest.java index 6f3481c99ca..2183f5284b1 100644 --- a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/common/thrift/text/TTextProtocolTest.java +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/common/thrift/text/TTextProtocolTest.java @@ -39,10 +39,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.EnumSet; import java.util.regex.Pattern; -import org.apache.commons.codec.binary.Base64; import org.apache.thrift.TApplicationException; import org.apache.thrift.TException; import org.apache.thrift.protocol.TMessage; @@ -59,6 +59,7 @@ import com.google.common.io.Resources; import com.linecorp.armeria.internal.common.thrift.TApplicationExceptions; +import com.linecorp.armeria.internal.server.thrift.ThriftVersionDetector; import testing.thrift.debug.RpcDebugService; import testing.thrift.debug.RpcDebugService.doDebug_args; @@ -85,7 +86,7 @@ class TTextProtocolTest { private String testData; private String namedEnumSerialized; - private Base64 base64Encoder; + private Base64.Decoder base64Decoder; /** * Load a file containing a serialized thrift message in from disk. @@ -94,7 +95,7 @@ class TTextProtocolTest { void setUp() throws IOException { testData = readFile("TTextProtocol_TestData.txt"); namedEnumSerialized = readFile("TTextProtocol_NamedEnum_Serialized.txt"); - base64Encoder = new Base64(); + base64Decoder = Base64.getDecoder(); } /** @@ -139,9 +140,18 @@ void tTextNamedEnumProtocolReadWriteTest() throws Exception { msg1.write(new TTextProtocol(new TIOStreamTransport(baos), true)); final byte[] bytes = baos.toByteArray(); + // y can be serialized to either 1 (<0.19) or ALPHA (>=0.19) assertThatJson(CR_PATTERN.matcher(baos.toString()).replaceAll("")) .when(IGNORING_ARRAY_ORDER) + .whenIgnoringPaths("y") .isEqualTo(namedEnumSerialized); + if (ThriftVersionDetector.majorVersion() == 0 && ThriftVersionDetector.minorVersion() < 19) { + assertThatJson(CR_PATTERN.matcher(baos.toString()).replaceAll("")) + .node("y").isEqualTo("1"); + } else { + assertThatJson(CR_PATTERN.matcher(baos.toString()).replaceAll("")) + .node("y").isEqualTo("ALPHA"); + } // Deserialize that string back to a thrift message. final ByteArrayInputStream bais2 = new ByteArrayInputStream(bytes); @@ -174,7 +184,7 @@ private TTextProtocolTestMsg testMsg() { (short) 5, ImmutableList.of(false) )) .setK(ImmutableSet.of(true, false, false, false, true)) - .setL(base64Encoder.decode("SGVsbG8gV29ybGQ=")) + .setL(base64Decoder.decode("SGVsbG8gV29ybGQ=")) .setM("hello \"spherical\" world!") .setN((short) 678) .setP(Letter.CHARLIE) @@ -190,7 +200,7 @@ private TTextProtocolTestMsg testMsg() { .setU(ImmutableMap.of("foo", Letter.ALPHA, "bar", Letter.DELTA)) .setV(Letter.BETA) .setW(TestUnion.f2(4)) - .setX(ImmutableList.of(TestUnion.f2(5), TestUnion.f1(base64Encoder.decode("SGVsbG8gV29ybGQ=")))) + .setX(ImmutableList.of(TestUnion.f2(5), TestUnion.f1(base64Decoder.decode("SGVsbG8gV29ybGQ=")))) .setY(Letter.ALPHA) .setAa(ImmutableMap.of(Letter.ALPHA, 2, Letter.BETA, 4)) .setAb(ImmutableMap.of(Letter.CHARLIE, Number.ONE, diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/ApacheClientUtils.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/ApacheClientUtils.java new file mode 100644 index 00000000000..9dc46dd6417 --- /dev/null +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/ApacheClientUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.internal; + +import javax.net.ssl.SSLContext; + +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.thrift.transport.THttpClient; + +public final class ApacheClientUtils { + + private ApacheClientUtils() {} + + public static THttpClient allTrustClient(String uri) { + try { + final SSLContext sslContext = + SSLContextBuilder.create() + .loadTrustMaterial((TrustStrategy) (chain, authType) -> true) + .build(); + return new THttpClient( + uri, HttpClientBuilder.create() + .setSSLHostnameVerifier((hostname, session) -> true) + .setSSLContext(sslContext) + .build()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServiceTest.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServiceTest.java index 4a0361f40c6..847319e0582 100644 --- a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServiceTest.java +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServiceTest.java @@ -26,10 +26,6 @@ import java.util.List; import java.util.Set; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.junit.ClassRule; import org.junit.Test; @@ -45,6 +41,7 @@ import com.linecorp.armeria.common.HttpHeaders; import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.SessionProtocol; import com.linecorp.armeria.common.thrift.ThriftSerializationFormats; import com.linecorp.armeria.internal.server.thrift.ThriftDocServicePlugin.Entry; import com.linecorp.armeria.internal.server.thrift.ThriftDocServicePlugin.EntryBuilder; @@ -257,12 +254,9 @@ public void excludeAllServices() throws IOException { @Test public void testMethodNotAllowed() throws Exception { - try (CloseableHttpClient hc = HttpClients.createMinimal()) { - final HttpPost req = new HttpPost(server.httpUri() + "/docs/specification.json"); - - try (CloseableHttpResponse res = hc.execute(req)) { - assertThat(res.getStatusLine().toString()).isEqualTo("HTTP/1.1 405 Method Not Allowed"); - } - } + final AggregatedHttpResponse res = + WebClient.of(server.uri(SessionProtocol.H1C, SerializationFormat.NONE)) + .blocking().post("/docs/specification.json", ""); + assertThat(res.status().code()).isEqualTo(405); } } diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftVersionDetector.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftVersionDetector.java new file mode 100644 index 00000000000..5f4c5cc0c47 --- /dev/null +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftVersionDetector.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.internal.server.thrift; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.linecorp.armeria.common.util.Version; + +public final class ThriftVersionDetector { + + private static final int majorVersion; + private static final int minorVersion; + + static { + final Pattern pattern = Pattern.compile("^armeria-thrift(?\\d+)\\.(?\\d+)$"); + final Entry thriftVersionEntry = + Version.getAll().keySet().stream() + .map(name -> { + final Matcher matcher = pattern.matcher(name); + if (!matcher.matches()) { + return null; + } + final int major = Integer.valueOf(matcher.group("major")); + final int minor = Integer.valueOf(matcher.group("minor")); + return new SimpleEntry<>(major, minor); + }) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> new RuntimeException("Couldn't find a thrift version")); + majorVersion = thriftVersionEntry.getKey(); + minorVersion = thriftVersionEntry.getValue(); + } + + public static int majorVersion() { + return majorVersion; + } + + public static int minorVersion() { + return minorVersion; + } + + private ThriftVersionDetector() {} +} diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/AbstractThriftOverHttpTest.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/AbstractThriftOverHttpTest.java index 107bced47d0..09203b909db 100644 --- a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/AbstractThriftOverHttpTest.java +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/AbstractThriftOverHttpTest.java @@ -30,10 +30,9 @@ import org.apache.thrift.protocol.TMessageType; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import com.google.common.base.Strings; @@ -50,52 +49,28 @@ import com.linecorp.armeria.common.thrift.ThriftReply; import com.linecorp.armeria.internal.testing.AnticipatedException; import com.linecorp.armeria.server.HttpService; -import com.linecorp.armeria.server.Server; import com.linecorp.armeria.server.ServerBuilder; -import com.linecorp.armeria.server.ServerPort; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.server.SimpleDecoratingHttpService; import com.linecorp.armeria.server.logging.LoggingService; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; import testing.thrift.main.HelloService; import testing.thrift.main.HelloService.AsyncIface; import testing.thrift.main.OnewayHelloService; import testing.thrift.main.SleepService; -public abstract class AbstractThriftOverHttpTest { +abstract class AbstractThriftOverHttpTest { private static final String LARGER_THAN_TLS = Strings.repeat("A", 16384); - private static final Server server; - - private static int httpPort; - private static int httpsPort; - private static volatile boolean recordMessageLogs; private static final BlockingQueue requestLogs = new LinkedBlockingQueue<>(); - abstract static class HelloServiceBase implements AsyncIface { + @RegisterExtension + static ServerExtension server = new ServerExtension() { @Override - @SuppressWarnings("unchecked") - public void hello(String name, AsyncMethodCallback resultHandler) throws TException { - resultHandler.onComplete(getResponse(name)); - } - - protected String getResponse(String name) { - return "Hello, " + name + '!'; - } - } - - static class HelloServiceChild extends HelloServiceBase { - @Override - protected String getResponse(String name) { - return "Goodbye, " + name + '!'; - } - } - - static { - final ServerBuilder sb = Server.builder(); - try { + protected void configure(ServerBuilder sb) throws Exception { sb.http(0); sb.https(0); sb.tlsSelfSigned(); @@ -149,37 +124,36 @@ public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exc }; sb.decorator(logCollectingDecorator); - } catch (Exception e) { - throw new Error(e); } - server = sb.build(); - } + }; - @BeforeClass - public static void init() throws Exception { - server.start().get(); + abstract static class HelloServiceBase implements AsyncIface { + @Override + @SuppressWarnings("unchecked") + public void hello(String name, AsyncMethodCallback resultHandler) throws TException { + resultHandler.onComplete(getResponse(name)); + } - httpPort = server.activePorts().values().stream() - .filter(ServerPort::hasHttp).findAny().get() - .localAddress().getPort(); - httpsPort = server.activePorts().values().stream() - .filter(ServerPort::hasHttps).findAny().get() - .localAddress().getPort(); + protected String getResponse(String name) { + return "Hello, " + name + '!'; + } } - @AfterClass - public static void destroy() throws Exception { - server.stop().get(); + static class HelloServiceChild extends HelloServiceBase { + @Override + protected String getResponse(String name) { + return "Goodbye, " + name + '!'; + } } - @Before - public void beforeTest() { + @BeforeEach + void beforeTest() { recordMessageLogs = false; requestLogs.clear(); } @Test - public void testHttpInvocation() throws Exception { + void testHttpInvocation() throws Exception { try (TTransport transport = newTransport("http", "/hello")) { final HelloService.Client client = new HelloService.Client.Factory().getClient( @@ -190,7 +164,7 @@ public void testHttpInvocation() throws Exception { } @Test - public void testInheritedThriftService() throws Exception { + void testInheritedThriftService() throws Exception { try (TTransport transport = newTransport("http", "/hellochild")) { final HelloService.Client client = new HelloService.Client.Factory().getClient( @@ -201,7 +175,7 @@ public void testInheritedThriftService() throws Exception { } @Test - public void testOnewaySyncInvocation() throws Exception { + void testOnewaySyncInvocation() throws Exception { recordMessageLogs = true; try (TTransport transport = newTransport("http", "/hello_oneway_sync")) { @@ -224,7 +198,7 @@ public void testOnewaySyncInvocation() throws Exception { } @Test - public void testOnewayAsyncInvocation() throws Exception { + void testOnewayAsyncInvocation() throws Exception { recordMessageLogs = true; try (TTransport transport = newTransport("http", "/hello_oneway_async")) { @@ -262,7 +236,7 @@ private static void verifyOneWayInvocation(Class expectedServiceType, String } @Test - public void testHttpsInvocation() throws Exception { + void testHttpsInvocation() throws Exception { try (TTransport transport = newTransport("https", "/hello")) { final HelloService.Client client = new HelloService.Client.Factory().getClient( @@ -273,7 +247,7 @@ public void testHttpsInvocation() throws Exception { } @Test - public void testLargeHttpsInvocation() throws Exception { + void testLargeHttpsInvocation() throws Exception { try (TTransport transport = newTransport("https", "/large")) { final HelloService.Client client = new HelloService.Client.Factory().getClient( @@ -284,7 +258,7 @@ public void testLargeHttpsInvocation() throws Exception { } @Test - public void testAcceptHeaderWithCommaSeparatedMediaTypes() throws Exception { + void testAcceptHeaderWithCommaSeparatedMediaTypes() throws Exception { try (TTransport transport = newTransport("http", "/hello", HttpHeaders.of(HttpHeaderNames.ACCEPT, "text/plain, */*"))) { final HelloService.Client client = @@ -296,7 +270,7 @@ public void testAcceptHeaderWithCommaSeparatedMediaTypes() throws Exception { } @Test - public void testAcceptHeaderWithQValues() throws Exception { + void testAcceptHeaderWithQValues() throws Exception { // Server should choose TBINARY because it has higher q-value (0.5) than that of TTEXT (0.2) try (TTransport transport = newTransport( "http", "/hello", @@ -312,7 +286,7 @@ public void testAcceptHeaderWithQValues() throws Exception { } @Test - public void testAcceptHeaderWithDefaultQValues() throws Exception { + void testAcceptHeaderWithDefaultQValues() throws Exception { // Server should choose TBINARY because it has higher q-value (default 1.0) than that of TTEXT (0.2) try (TTransport transport = newTransport( "http", "/hello", @@ -328,7 +302,7 @@ public void testAcceptHeaderWithDefaultQValues() throws Exception { } @Test - public void testAcceptHeaderWithUnsupportedMediaTypes() throws Exception { + void testAcceptHeaderWithUnsupportedMediaTypes() throws Exception { // Server should choose TBINARY because it does not support the media type // with the highest preference (text/plain). try (TTransport transport = newTransport( @@ -343,8 +317,8 @@ public void testAcceptHeaderWithUnsupportedMediaTypes() throws Exception { } } - @Test(timeout = 10000) - public void testMessageLogsForCall() throws Exception { + @Test + void testMessageLogsForCall() throws Exception { try (TTransport transport = newTransport("http", "/hello")) { final HelloService.Client client = new HelloService.Client.Factory().getClient( @@ -385,8 +359,8 @@ public void testMessageLogsForCall() throws Exception { .isEqualTo("Hello, Trustin!"); } - @Test(timeout = 10000) - public void testMessageLogsForException() throws Exception { + @Test + void testMessageLogsForException() throws Exception { try (TTransport transport = newTransport("http", "/exception")) { final HelloService.Client client = new HelloService.Client.Factory().getClient( @@ -439,9 +413,9 @@ protected final TTransport newTransport(String scheme, String path, protected static String newUri(String scheme, String path) { switch (scheme) { case "http": - return scheme + "://127.0.0.1:" + httpPort + path; + return scheme + "://127.0.0.1:" + server.httpPort() + path; case "https": - return scheme + "://127.0.0.1:" + httpsPort + path; + return scheme + "://127.0.0.1:" + server.httpsPort() + path; } throw new Error(); diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/ThriftOverHttp1Test.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/ThriftOverHttp1Test.java index 1fbbcbfaba8..3b7fb39c84b 100644 --- a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/ThriftOverHttp1Test.java +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/ThriftOverHttp1Test.java @@ -18,49 +18,32 @@ import static com.google.common.collect.ImmutableMap.toImmutableMap; import static org.assertj.core.api.Assertions.assertThat; -import java.security.GeneralSecurityException; - -import javax.net.ssl.SSLContext; - -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.conn.ssl.TrustStrategy; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.ssl.SSLContextBuilder; -import org.apache.http.util.EntityUtils; import org.apache.thrift.transport.THttpClient; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; import com.linecorp.armeria.common.HttpHeaders; +import com.linecorp.armeria.common.HttpMethod; +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.SessionProtocol; import com.linecorp.armeria.common.thrift.ThriftProtocolFactories; +import com.linecorp.armeria.internal.ApacheClientUtils; import io.netty.util.AsciiString; import testing.thrift.main.SleepService; -public class ThriftOverHttp1Test extends AbstractThriftOverHttpTest { +class ThriftOverHttp1Test extends AbstractThriftOverHttpTest { + @Override protected TTransport newTransport(String uri, HttpHeaders headers) throws TTransportException { - final SSLContext sslContext; - try { - sslContext = SSLContextBuilder.create() - .loadTrustMaterial((TrustStrategy) (chain, authType) -> true) - .build(); - } catch (GeneralSecurityException e) { - throw new TTransportException("failed to initialize an SSL context", e); - } - - final THttpClient client = new THttpClient( - uri, HttpClientBuilder.create() - .setSSLHostnameVerifier((hostname, session) -> true) - .setSSLContext(sslContext) - .build()); + final THttpClient client = ApacheClientUtils.allTrustClient(uri); client.setCustomHeaders( headers.names().stream() .collect(toImmutableMap(AsciiString::toString, @@ -68,28 +51,20 @@ protected TTransport newTransport(String uri, HttpHeaders headers) throws TTrans return client; } - @Test - public void testNonPostRequest() throws Exception { - final HttpUriRequest[] reqs = { - new HttpGet(newUri("http", "/hello")), - new HttpDelete(newUri("http", "/hello")) - }; - - try (CloseableHttpClient hc = HttpClients.createMinimal()) { - for (HttpUriRequest r: reqs) { - try (CloseableHttpResponse res = hc.execute(r)) { - assertThat(res.getStatusLine().toString()).isEqualTo( - "HTTP/1.1 405 Method Not Allowed"); - assertThat(EntityUtils.toString(res.getEntity())) - .isNotEqualTo("Hello, world!"); - } - } - } + @ParameterizedTest + @EnumSource(value = HttpMethod.class, names = {"DELETE", "GET"}) + void testNonPostRequest(HttpMethod method) throws Exception { + final AggregatedHttpResponse res = + WebClient.of(server.uri(SessionProtocol.H1C, SerializationFormat.NONE)) + .blocking() + .execute(HttpRequest.of(method, "/hello")); + assertThat(res.status().code()).isEqualTo(405); + assertThat(res.contentUtf8()).doesNotContain("Hello, world!"); } @Test - @Ignore - public void testPipelinedHttpInvocation() throws Exception { + @Disabled + void testPipelinedHttpInvocation() throws Exception { // FIXME: Enable this test once we have a working Thrift-over-HTTP/1 client with pipelining. try (TTransport transport = newTransport("http", "/sleep")) { final SleepService.Client client = new SleepService.Client.Factory().getClient( diff --git a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/ThriftOverHttp2Test.java b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/ThriftOverHttp2Test.java index 97c328b5f59..489f4cc39f0 100644 --- a/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/ThriftOverHttp2Test.java +++ b/thrift/thrift0.13/src/test/java/com/linecorp/armeria/server/thrift/ThriftOverHttp2Test.java @@ -22,7 +22,7 @@ import io.netty.handler.codec.http.DefaultHttpHeaders; -public class ThriftOverHttp2Test extends AbstractThriftOverHttpTest { +class ThriftOverHttp2Test extends AbstractThriftOverHttpTest { @Override protected TTransport newTransport(String uri, HttpHeaders headers) throws TTransportException { final io.netty.handler.codec.http.HttpHeaders nettyDefaultHeaders = new DefaultHttpHeaders(); diff --git a/thrift/thrift0.19/build.gradle b/thrift/thrift0.19/build.gradle new file mode 100644 index 00000000000..92501298efa --- /dev/null +++ b/thrift/thrift0.19/build.gradle @@ -0,0 +1,91 @@ +// This module builds and publishes 'armeria-thrift0.19', which is compiled with +// the source code of ':thrift0.13', ':thrift0.14.0'. + +dependencies { + api libs.thrift019 + + api libs.javax.annotation + + testImplementation project(':prometheus1') + + // thrift api depends on httpclient5 + testImplementation libs.apache.httpclient5 + + // Jetty, for testing TServlet interoperability. + testImplementation libs.jetty11.webapp + testImplementation libs.jetty11.http2.server + + // Dropwizard and Prometheus, for testing metrics integration + testImplementation libs.dropwizard.metrics.core + testImplementation libs.prometheus.metrics.exposition.formats + + // micrometer tracing + testImplementation (libs.micrometer.tracing.integration.test) { + exclude group: "org.mockito" + } + testImplementation libs.brave6.instrumentation.http.tests + testImplementation libs.logback14 +} + +// Use the sources from ':thrift0.13' and ':thrift0.14'. ':thrift0.15' is empty so no need to copy anything. +// NB: We should never add these directories using the 'sourceSets' directive because that will make +// them added to more than one project and having a source directory with more than one output directory +// will confuse IDEs such as IntelliJ IDEA. +def thrift013ProjectDir = "${rootProject.projectDir}/thrift/thrift0.13" +def thrift014ProjectDir = "${rootProject.projectDir}/thrift/thrift0.14" +def thrift017ProjectDir = "${rootProject.projectDir}/thrift/thrift0.17" +def thrift018ProjectDir = "${rootProject.projectDir}/thrift/thrift0.18" + +// Copy common files from ':thrift0.13' and ':thrift0.14' to gen-src directory +// in order to use them as a source set. +task generateSources(type: Copy) { + from("${thrift013ProjectDir}/src/main/java") { + exclude '**/TByteBufTransport.java' + exclude '**/common/thrift/package-info.java' + } + from "${thrift014ProjectDir}/src/main/java" + into "${project.ext.genSrcDir}/main/java" +} + +task generateTestSources(type: Copy) { + from("${thrift013ProjectDir}/src/test/java") { + exclude '**/THttp2Client.java' + exclude '**/ThriftDocServicePluginTest.java' + exclude '**/ApacheClientUtils.java' + exclude '**/ServletTestUtils.java' + } + from "${thrift014ProjectDir}/src/test/java" + from "${thrift017ProjectDir}/src/test/java" + from "${thrift018ProjectDir}/src/test/java" + into "${project.ext.genSrcDir}/test/java" +} + +def thriftFullVersion = libs.thrift019.get().versionConstraint.requiredVersion +ext { + thriftVersion = thriftFullVersion.substring(0, thriftFullVersion.lastIndexOf('.')); + testThriftSrcDirs = ["$thrift013ProjectDir/src/test/thrift", "$projectDir/src/test/thrift", + "$thrift018ProjectDir/src/test/thrift"] +} + +tasks.generateSources.dependsOn(generateTestSources) +tasks.compileJava.dependsOn(generateSources) +tasks.compileTestJava.dependsOn(generateSources) + +tasks.processResources.from("${thrift013ProjectDir}/src/main/resources") { + exclude '**/thrift-options.properties' +} +tasks.processTestResources.from "${thrift013ProjectDir}/src/test/resources" +tasks.sourcesJar.from "${thrift013ProjectDir}/src/main/resources" + +// Keep the original Guava references in ThriftListenableFuture, +// which is the only place we expose Guava classes in our public API. +// NB: Keep this same with ':thrift0.13'. +tasks.shadedJar.exclude 'com/linecorp/armeria/common/thrift/ThriftListenableFuture*' +tasks.shadedJar.doLast { + ant.jar(update: true, destfile: tasks.shadedJar.archiveFile.get().asFile) { + sourceSets.main.output.classesDirs.each { classesDir -> + fileset(dir: "$classesDir", + includes: 'com/linecorp/armeria/common/thrift/ThriftListenableFuture*') + } + } +} diff --git a/thrift/thrift0.19/src/main/resources/com/linecorp/armeria/internal/common/thrift/thrift-options.properties b/thrift/thrift0.19/src/main/resources/com/linecorp/armeria/internal/common/thrift/thrift-options.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/thrift/thrift0.19/src/test/java/com/linecorp/armeria/client/thrift/ServletTestUtils.java b/thrift/thrift0.19/src/test/java/com/linecorp/armeria/client/thrift/ServletTestUtils.java new file mode 100644 index 00000000000..54b86d3bd19 --- /dev/null +++ b/thrift/thrift0.19/src/test/java/com/linecorp/armeria/client/thrift/ServletTestUtils.java @@ -0,0 +1,154 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.client.thrift; + +import static org.assertj.core.api.Assertions.fail; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.thrift.server.TServlet; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +import com.linecorp.armeria.common.thrift.ThriftProtocolFactories; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import testing.thrift.main.HelloService.Processor; + +final class ServletTestUtils { + + private ServletTestUtils() {} + + static final String TSERVLET_PATH = "/thrift"; + + @SuppressWarnings("unchecked") + private static final Servlet thriftServlet = + new TServlet(new Processor(name -> "Hello, " + name + '!'), ThriftProtocolFactories.binary(0, 0)); + + private static final Servlet rootServlet = new HttpServlet() { + private static final long serialVersionUID = 6765028749367036441L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + resp.setStatus(404); + resp.setContentLength(0); + } + }; + + static Server startHttp1(AtomicBoolean sendConnectionClose) throws Exception { + final Server server = new Server(0); + + final ServletHandler handler = new ServletHandler(); + handler.addServletWithMapping(newServletHolder(thriftServlet), TSERVLET_PATH); + handler.addServletWithMapping(newServletHolder(rootServlet), "/"); + handler.addFilterWithMapping(new FilterHolder(new ConnectionCloseFilter(sendConnectionClose)), "/*", + EnumSet.of(DispatcherType.REQUEST)); + + server.setHandler(handler); + + for (Connector c : server.getConnectors()) { + for (ConnectionFactory f : c.getConnectionFactories()) { + for (String p : f.getProtocols()) { + if (p.startsWith("h2c")) { + fail("Attempted to create a Jetty server without HTTP/2 support, but failed: " + + f.getProtocols()); + } + } + } + } + + server.start(); + return server; + } + + static Server startHttp2() throws Exception { + final Server server = new Server(); + // Common HTTP configuration. + final HttpConfiguration config = new HttpConfiguration(); + + // HTTP/1.1 support. + final HttpConnectionFactory http1 = new HttpConnectionFactory(config); + + // HTTP/2 cleartext support. + final HTTP2CServerConnectionFactory http2c = new HTTP2CServerConnectionFactory(config); + + // Add the connector. + final ServerConnector connector = new ServerConnector(server, http1, http2c); + connector.setPort(0); + server.addConnector(connector); + + // Add the servlet. + final ServletHandler handler = new ServletHandler(); + handler.addServletWithMapping(newServletHolder(thriftServlet), TSERVLET_PATH); + server.setHandler(handler); + + // Start the server. + server.start(); + return server; + } + + private static ServletHolder newServletHolder(Servlet rootServlet) { + final ServletHolder holder = new ServletHolder(rootServlet); + holder.setInitParameter("cacheControl", "max-age=0, public"); + return holder; + } + + private static class ConnectionCloseFilter implements Filter { + private final AtomicBoolean sendConnectionClose; + + ConnectionCloseFilter(AtomicBoolean sendConnectionClose) { + this.sendConnectionClose = sendConnectionClose; + } + + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + chain.doFilter(request, response); + + if (sendConnectionClose.get()) { + ((HttpServletResponse) response).setHeader("Connection", "close"); + } + } + + @Override + public void destroy() {} + } +} diff --git a/thrift/thrift0.19/src/test/java/com/linecorp/armeria/internal/ApacheClientUtils.java b/thrift/thrift0.19/src/test/java/com/linecorp/armeria/internal/ApacheClientUtils.java new file mode 100644 index 00000000000..e0b1efc433c --- /dev/null +++ b/thrift/thrift0.19/src/test/java/com/linecorp/armeria/internal/ApacheClientUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.internal; + +import java.util.AbstractMap; +import java.util.Map.Entry; + +import org.apache.hc.client5.http.classic.methods.HttpDelete; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpUriRequest; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.client5.http.ssl.TrustAllStrategy; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.thrift.transport.THttpClient; + +import com.linecorp.armeria.common.HttpMethod; + +public final class ApacheClientUtils { + + private ApacheClientUtils() {} + + public static THttpClient allTrustClient(String uri) { + CloseableHttpClient httpclient = null; + try { + final PoolingHttpClientConnectionManager connManager = PoolingHttpClientConnectionManagerBuilder + .create() + .setSSLSocketFactory( + SSLConnectionSocketFactoryBuilder + .create() + .setSslContext( + SSLContextBuilder.create() + .loadTrustMaterial( + TrustAllStrategy.INSTANCE) + .build()) + .setHostnameVerifier( + NoopHostnameVerifier.INSTANCE) + .build()) + .build(); + httpclient = HttpClients + .custom() + .setConnectionManager(connManager) + .build(); + return new THttpClient(uri, httpclient); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static Entry makeApacheHttpRequest(String uri, HttpMethod method) { + final HttpUriRequest req; + switch (method) { + case GET: + req = new HttpGet(uri); + break; + case DELETE: + req = new HttpDelete(uri); + break; + case POST: + req = new HttpPost(uri); + break; + default: + throw new UnsupportedOperationException("Unsupported HTTP method: " + method); + } + try (CloseableHttpClient hc = HttpClients.createMinimal()) { + try (CloseableHttpResponse res = hc.execute(req)) { + return new AbstractMap.SimpleEntry<>(res.getVersion().toString(), + EntityUtils.toString(res.getEntity())); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/thrift/thrift0.20/build.gradle b/thrift/thrift0.20/build.gradle new file mode 100644 index 00000000000..b960e0d7a6b --- /dev/null +++ b/thrift/thrift0.20/build.gradle @@ -0,0 +1,93 @@ +// This module builds and publishes 'armeria-thrift0.20', which is compiled with +// the source code of ':thrift0.13', ':thrift0.14.0'. + +dependencies { + api libs.thrift020 + + api libs.javax.annotation + + testImplementation project(':prometheus1') + + // thrift api depends on httpclient5 + testImplementation libs.apache.httpclient5 + + // Jetty, for testing TServlet interoperability. + testImplementation libs.jetty11.webapp + testImplementation libs.jetty11.http2.server + + // Dropwizard and Prometheus, for testing metrics integration + testImplementation libs.dropwizard.metrics.core + testImplementation libs.prometheus.metrics.exposition.formats + + // micrometer tracing + testImplementation (libs.micrometer.tracing.integration.test) { + exclude group: "org.mockito" + } + testImplementation libs.brave6.instrumentation.http.tests + testImplementation libs.logback14 +} + +// Use the sources from ':thrift0.13' and ':thrift0.14'. ':thrift0.15' is empty so no need to copy anything. +// NB: We should never add these directories using the 'sourceSets' directive because that will make +// them added to more than one project and having a source directory with more than one output directory +// will confuse IDEs such as IntelliJ IDEA. +def thrift013ProjectDir = "${rootProject.projectDir}/thrift/thrift0.13" +def thrift014ProjectDir = "${rootProject.projectDir}/thrift/thrift0.14" +def thrift017ProjectDir = "${rootProject.projectDir}/thrift/thrift0.17" +def thrift018ProjectDir = "${rootProject.projectDir}/thrift/thrift0.18" +def thrift019ProjectDir = "${rootProject.projectDir}/thrift/thrift0.19" + +// Copy common files from ':thrift0.13' and ':thrift0.14' to gen-src directory +// in order to use them as a source set. +task generateSources(type: Copy) { + from("${thrift013ProjectDir}/src/main/java") { + exclude '**/TByteBufTransport.java' + exclude '**/common/thrift/package-info.java' + } + from "${thrift014ProjectDir}/src/main/java" + into "${project.ext.genSrcDir}/main/java" +} + +task generateTestSources(type: Copy) { + from("${thrift013ProjectDir}/src/test/java") { + exclude '**/THttp2Client.java' + exclude '**/ThriftDocServicePluginTest.java' + exclude '**/ApacheClientUtils.java' + exclude '**/ServletTestUtils.java' + } + from "${thrift014ProjectDir}/src/test/java" + from "${thrift017ProjectDir}/src/test/java" + from "${thrift018ProjectDir}/src/test/java" + from "${thrift019ProjectDir}/src/test/java" + into "${project.ext.genSrcDir}/test/java" +} + +def thriftFullVersion = libs.thrift020.get().versionConstraint.requiredVersion +ext { + thriftVersion = thriftFullVersion.substring(0, thriftFullVersion.lastIndexOf('.')); + testThriftSrcDirs = ["$thrift013ProjectDir/src/test/thrift", "$projectDir/src/test/thrift", + "$thrift018ProjectDir/src/test/thrift"] +} + +tasks.generateSources.dependsOn(generateTestSources) +tasks.compileJava.dependsOn(generateSources) +tasks.compileTestJava.dependsOn(generateSources) + +tasks.processResources.from("${thrift013ProjectDir}/src/main/resources") { + exclude '**/thrift-options.properties' +} +tasks.processTestResources.from "${thrift013ProjectDir}/src/test/resources" +tasks.sourcesJar.from "${thrift013ProjectDir}/src/main/resources" + +// Keep the original Guava references in ThriftListenableFuture, +// which is the only place we expose Guava classes in our public API. +// NB: Keep this same with ':thrift0.13'. +tasks.shadedJar.exclude 'com/linecorp/armeria/common/thrift/ThriftListenableFuture*' +tasks.shadedJar.doLast { + ant.jar(update: true, destfile: tasks.shadedJar.archiveFile.get().asFile) { + sourceSets.main.output.classesDirs.each { classesDir -> + fileset(dir: "$classesDir", + includes: 'com/linecorp/armeria/common/thrift/ThriftListenableFuture*') + } + } +} diff --git a/thrift/thrift0.20/src/main/resources/com/linecorp/armeria/internal/common/thrift/thrift-options.properties b/thrift/thrift0.20/src/main/resources/com/linecorp/armeria/internal/common/thrift/thrift-options.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java index 12a7b017384..e11756065ae 100644 --- a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java +++ b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java @@ -27,6 +27,8 @@ import org.apache.tomcat.util.net.AbstractEndpoint; import org.apache.tomcat.util.net.SocketWrapperBase; +import com.linecorp.armeria.common.annotation.Nullable; + import jakarta.servlet.ServletConnection; /** @@ -87,6 +89,7 @@ protected boolean isTrailerFieldsReady() { return false; } + @Nullable @Override protected ServletConnection getServletConnection() { return null; @@ -102,6 +105,7 @@ protected AbstractEndpoint.Handler.SocketState dispatchEndRequest() throws IOExc throw new UnsupportedOperationException(); } + @Nullable @Override protected AbstractEndpoint.Handler.SocketState service(SocketWrapperBase socketWrapper) throws IOException { diff --git a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/ManagedTomcatService.java b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/ManagedTomcatService.java index 600582f98d5..aec032492eb 100644 --- a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/ManagedTomcatService.java +++ b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/ManagedTomcatService.java @@ -135,11 +135,13 @@ void stop() throws Exception { postStopTask.accept(connector); } + @Nullable @Override public Connector connector() { return connector; } + @Nullable @Override String hostName() { return hostName; diff --git a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatService.java b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatService.java index 5b89435f336..13d3c159d61 100644 --- a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatService.java +++ b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatService.java @@ -513,6 +513,7 @@ private Request convertRequest(ServiceRequestContext ctx, String mappedPath, Agg coyoteReq.setLocalPort(localAddr.getPort()); final String hostHeader = req.authority(); + assert hostHeader != null; final int colonPos = hostHeader.indexOf(':'); if (colonPos < 0) { coyoteReq.serverName().setString(hostHeader); diff --git a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatUtil.java b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatUtil.java index f9f6361eba9..d594c4decc4 100644 --- a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatUtil.java +++ b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatUtil.java @@ -34,10 +34,13 @@ import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.Tomcat.DefaultWebXmlListener; +import com.linecorp.armeria.common.annotation.Nullable; + final class TomcatUtil { private static final LifecycleListener defaultWebXmlListener = new DefaultWebXmlListener(); + @Nullable static URL getWebAppConfigFile(String contextPath, Path docBase) { final AtomicReference configUrlRef = new AtomicReference<>(); new Tomcat() { diff --git a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/UnmanagedTomcatService.java b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/UnmanagedTomcatService.java index c4941068b60..60f74b36578 100644 --- a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/UnmanagedTomcatService.java +++ b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/UnmanagedTomcatService.java @@ -54,6 +54,7 @@ public Connector connector() { return tomcat.getConnector(); } + @Nullable @Override public String hostName() { if (hostName != null) { diff --git a/tomcat8/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaEndpoint.java b/tomcat8/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaEndpoint.java index f4aa0cd2f48..e25a2d98420 100644 --- a/tomcat8/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaEndpoint.java +++ b/tomcat8/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaEndpoint.java @@ -26,6 +26,8 @@ import org.apache.tomcat.util.net.SocketProcessorBase; import org.apache.tomcat.util.net.SocketWrapperBase; +import com.linecorp.armeria.common.annotation.Nullable; + /** * A fake {@link AbstractEndpoint}. */ @@ -43,6 +45,7 @@ protected void createSSLContext(SSLHostConfig sslHostConfig) throws Exception {} @Override protected void setDefaultSslHostConfig(SSLHostConfig sslHostConfig) {} + @Nullable @Override protected InetSocketAddress getLocalAddress() throws IOException { // Doesn't seem to be used. @@ -59,6 +62,7 @@ protected boolean getDeferAccept() { return false; } + @Nullable @Override protected SocketProcessorBase createSocketProcessor(SocketWrapperBase socketWrapper, SocketEvent event) { return null; @@ -84,6 +88,7 @@ protected Log getLog() { @Override protected void doCloseServerSocket() throws IOException {} + @Nullable @Override protected Object serverSocketAccept() throws Exception { return null; diff --git a/tomcat8/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java b/tomcat8/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java index eaa273bdf73..53ebd129599 100644 --- a/tomcat8/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java +++ b/tomcat8/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java @@ -26,6 +26,8 @@ import org.apache.tomcat.util.net.AbstractEndpoint; import org.apache.tomcat.util.net.SocketWrapperBase; +import com.linecorp.armeria.common.annotation.Nullable; + /** * Provides a fake Processor to provide {@link ActionHook} to request/response. */ @@ -89,6 +91,7 @@ protected AbstractEndpoint.Handler.SocketState dispatchEndRequest() throws IOExc throw new UnsupportedOperationException(); } + @Nullable @Override protected AbstractEndpoint.Handler.SocketState service(SocketWrapperBase socketWrapper) throws IOException { diff --git a/tomcat9/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java b/tomcat9/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java index 9c53581df68..7a3029eb2d2 100644 --- a/tomcat9/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java +++ b/tomcat9/src/main/java/com/linecorp/armeria/server/tomcat/ArmeriaProcessor.java @@ -26,6 +26,8 @@ import org.apache.tomcat.util.net.AbstractEndpoint; import org.apache.tomcat.util.net.SocketWrapperBase; +import com.linecorp.armeria.common.annotation.Nullable; + /** * Provides a fake Processor to provide {@code ActionHook} to request/response. */ @@ -94,6 +96,7 @@ protected AbstractEndpoint.Handler.SocketState dispatchEndRequest() throws IOExc throw new UnsupportedOperationException(); } + @Nullable @Override protected AbstractEndpoint.Handler.SocketState service(SocketWrapperBase socketWrapper) throws IOException { diff --git a/xds/src/main/java/com/linecorp/armeria/xds/AbstractResourceNode.java b/xds/src/main/java/com/linecorp/armeria/xds/AbstractResourceNode.java index 0b55c0c6ce6..a1200b99cd5 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/AbstractResourceNode.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/AbstractResourceNode.java @@ -54,6 +54,7 @@ XdsBootstrapImpl xdsBootstrap() { return xdsBootstrap; } + @Nullable @Override public ConfigSource configSource() { return configSource; @@ -63,6 +64,7 @@ private void setCurrent(@Nullable T current) { this.current = current; } + @Nullable @Override public T currentResource() { return current; diff --git a/xds/src/main/java/com/linecorp/armeria/xds/AbstractRoot.java b/xds/src/main/java/com/linecorp/armeria/xds/AbstractRoot.java index 29db1042a28..d5daeccba44 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/AbstractRoot.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/AbstractRoot.java @@ -107,7 +107,7 @@ public void snapshotUpdated(T newSnapshot) { return; } snapshot = newSnapshot; - notifyWatchers("snapshotUpdated", watcher -> watcher.snapshotUpdated(snapshot)); + notifyWatchers("snapshotUpdated", watcher -> watcher.snapshotUpdated(newSnapshot)); } @Override diff --git a/xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java b/xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java index 18dfef4d719..0b2a96f4f28 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java @@ -54,6 +54,7 @@ ClusterXdsResource withPrimer(@Nullable XdsResource primer) { return new ClusterXdsResource(cluster, primer); } + @Nullable @Override XdsResource primer() { return primer; diff --git a/xds/src/main/java/com/linecorp/armeria/xds/CompositeXdsStream.java b/xds/src/main/java/com/linecorp/armeria/xds/CompositeXdsStream.java index d9ba15918d4..720fbf9c58b 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/CompositeXdsStream.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/CompositeXdsStream.java @@ -49,6 +49,8 @@ public void close() { @Override public void resourcesUpdated(XdsType type) { - streamMap.get(type).resourcesUpdated(type); + final XdsStream stream = streamMap.get(type); + assert stream != null; + stream.resourcesUpdated(type); } } diff --git a/xds/src/main/java/com/linecorp/armeria/xds/SotwXdsStream.java b/xds/src/main/java/com/linecorp/armeria/xds/SotwXdsStream.java index c0c3e77a48d..6ef26f321f7 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/SotwXdsStream.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/SotwXdsStream.java @@ -166,7 +166,10 @@ void sendDiscoveryRequest(XdsType type, @Nullable String version, Collection requestObserver.onNext(request), + eventLoop.schedule(() -> { + assert requestObserver != null; + requestObserver.onNext(request); + }, backoff.nextDelayMillis(ackBackoffAttempts), TimeUnit.MILLISECONDS); } else { ackBackoffAttempts = 0; diff --git a/xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java b/xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java index c799ad1d947..5da380d02d0 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java @@ -45,7 +45,9 @@ final class XdsResourceParserUtil { } static ResourceParser fromType(XdsType xdsType) { - return typeToResourceType.get(xdsType); + final ResourceParser parser = typeToResourceType.get(xdsType); + assert parser != null; + return parser; } private XdsResourceParserUtil() {} diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLbStateFactory.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLbStateFactory.java index 5691af39637..1407326ac0d 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLbStateFactory.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLbStateFactory.java @@ -79,6 +79,7 @@ private static PerPriorityLoad calculatePerPriorityLoad(PrioritySet prioritySet) private static HealthAndDegraded recalculatePerPriorityState( int priority, PrioritySet prioritySet) { final HostSet hostSet = prioritySet.hostSets().get(priority); + assert hostSet != null; final int hostCount = hostSet.hosts().size(); if (hostCount <= 0) { @@ -188,6 +189,7 @@ private static PerPriorityPanic recalculatePerPriorityPanic(PrioritySet priority final ImmutableMap.Builder perPriorityPanicBuilder = ImmutableMap.builder(); for (Integer priority : prioritySet.priorities()) { final HostSet hostSet = prioritySet.hostSets().get(priority); + assert hostSet != null; final boolean isPanic = normalizedTotalAvailability == 100 ? false : isHostSetInPanic(hostSet, panicThreshold); perPriorityPanicBuilder.put(priority, isPanic); @@ -212,6 +214,7 @@ private static PerPriorityLoad recalculateLoadInTotalPanic(PrioritySet priorityS new Int2IntOpenHashMap(prioritySet.priorities().size()); for (Integer priority: prioritySet.priorities()) { final HostSet hostSet = prioritySet.hostSets().get(priority); + assert hostSet != null; final int hostsSize = hostSet.hosts().size(); if (firstNoEmpty == -1 && hostsSize > 0) { firstNoEmpty = priority; diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLoadBalancer.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLoadBalancer.java index 5d000f65fde..e2bd1b89857 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLoadBalancer.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLoadBalancer.java @@ -108,6 +108,7 @@ HostsSource hostSourceToUse(DefaultLbState lbState, XdsRandom random, final PrioritySet prioritySet = lbState.prioritySet(); final int priority = priorityAndAvailability.priority; final HostSet hostSet = prioritySet.hostSets().get(priority); + assert hostSet != null; final HostAvailability hostAvailability = priorityAndAvailability.hostAvailability; if (lbState.perPriorityPanic().get(priority)) { if (prioritySet.failTrafficOnPanic()) { diff --git a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java index bb8ef57abd4..79bd39f8d30 100644 --- a/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java +++ b/xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java @@ -36,6 +36,7 @@ import com.linecorp.armeria.client.endpoint.EndpointGroup; import com.linecorp.armeria.client.endpoint.EndpointSelectionStrategy; import com.linecorp.armeria.common.Flags; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; import com.linecorp.armeria.common.util.AbstractListenable; import com.linecorp.armeria.internal.common.util.ReentrantShortLock; @@ -138,6 +139,7 @@ private void maybeCompleteInitialEndpointsFuture(List endpoints) { } } + @Nullable @Override protected List latestValue() { final List endpoints = state.endpoints(); @@ -163,6 +165,7 @@ public EndpointSelectionStrategy selectionStrategy() { return selectionStrategy; } + @Nullable @Override public Endpoint selectNow(ClientRequestContext ctx) { return selector.selectNow(ctx); diff --git a/zookeeper3/src/main/java/com/linecorp/armeria/client/zookeeper/LegacyZooKeeperDiscoverySpec.java b/zookeeper3/src/main/java/com/linecorp/armeria/client/zookeeper/LegacyZooKeeperDiscoverySpec.java index 59d89268801..9446e9bcf7e 100644 --- a/zookeeper3/src/main/java/com/linecorp/armeria/client/zookeeper/LegacyZooKeeperDiscoverySpec.java +++ b/zookeeper3/src/main/java/com/linecorp/armeria/client/zookeeper/LegacyZooKeeperDiscoverySpec.java @@ -18,11 +18,13 @@ import javax.annotation.Nonnull; import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.internal.common.zookeeper.LegacyNodeValueCodec; enum LegacyZooKeeperDiscoverySpec implements ZooKeeperDiscoverySpec { INSTANCE; + @Nullable @Override public String path() { return null; diff --git a/zookeeper3/src/main/java/com/linecorp/armeria/client/zookeeper/ServerSetsDiscoverySpec.java b/zookeeper3/src/main/java/com/linecorp/armeria/client/zookeeper/ServerSetsDiscoverySpec.java index 43d001a04be..3ba695f44e1 100644 --- a/zookeeper3/src/main/java/com/linecorp/armeria/client/zookeeper/ServerSetsDiscoverySpec.java +++ b/zookeeper3/src/main/java/com/linecorp/armeria/client/zookeeper/ServerSetsDiscoverySpec.java @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory; import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.zookeeper.ServerSetsInstance; import com.linecorp.armeria.internal.common.zookeeper.ServerSetsNodeValueCodec; @@ -34,11 +35,13 @@ final class ServerSetsDiscoverySpec implements ZooKeeperDiscoverySpec { this.converter = converter; } + @Nullable @Override public String path() { return null; } + @Nullable @Override public Endpoint decode(byte[] data) { final ServerSetsInstance decodedInstance = ServerSetsNodeValueCodec.INSTANCE.decode(data); diff --git a/zookeeper3/src/main/java/com/linecorp/armeria/common/zookeeper/AbstractCuratorFrameworkBuilder.java b/zookeeper3/src/main/java/com/linecorp/armeria/common/zookeeper/AbstractCuratorFrameworkBuilder.java index e85d508df5f..4721f5ee760 100644 --- a/zookeeper3/src/main/java/com/linecorp/armeria/common/zookeeper/AbstractCuratorFrameworkBuilder.java +++ b/zookeeper3/src/main/java/com/linecorp/armeria/common/zookeeper/AbstractCuratorFrameworkBuilder.java @@ -50,7 +50,8 @@ public class AbstractCuratorFrameworkBuilder> customizers; @@ -174,6 +175,7 @@ public SELF sessionTimeoutMillis(long sessionTimeoutMillis) { public SELF customizer( Consumer customizer) { ensureInternalClient(); + assert customizers != null; customizers.add(requireNonNull(customizer, "customizer")); return self(); } @@ -202,6 +204,8 @@ protected final CuratorFramework buildCuratorFramework() { if (client != null) { return client; } + assert customizers != null; + assert clientBuilder != null; customizers.build().forEach(c -> c.accept(clientBuilder)); return clientBuilder.build(); }