0.99.7 release notes

22nd June 2020

🌟 New features

  • You can now specify the maximum lifespan of server-side connections using ServerBuilder.maxConnectionAge(). This is useful when you have to deal with a load balancer without HTTP/2 support. #2747 #2796

    Server server =
        Server.builder()
              .maxConnectionAge(Duration.ofMinutes(1))
              ...
              .build();
  • You can now record the name of the service that handled a request into RequestOnlyLog.serviceName(). #2768 #2780 #2809 #2820

    • By using ServiceBindingBuilder.defaultServiceName():

      Server server =
          Server.builder()
                .route().path("/service")
                        .defaultServiceName("my-service")
                        .build(new MyService())
                .build();
    • By using @ServiceName:

      @ServiceName("my-service")
      public class MyAnnotatedService {
          @Get("/get")
          public String get() { ... }
      
          @Post("/set")
          @ServiceName("my-post-service")
          public String post(@Param String value) { ... }
      }
    • Programmatically:

      Server server =
          Server.builder()
                .service("/service", (ctx, req) -> {
                    ctx.logBuilder().serviceName("my-service");
                })
                .build();
    • Armeria will use the FQCN of the service class if you did not specify a service name.

  • You can now use @Nullable annotation to specify an optional parameter or request object in annotated services. Previously, only the parameters with @Default annotation or Optional type were accepted. #2773

    public class MyAnnotatedService {
        // null will be injected into 'value' instead of returning
        // '400 Bad Request' when 'value' is missing.
        @Get("/get")
        public String get(@Param @Nullable String value) { ... }
    }
  • You can now use the classes in the java.time package in annotated services. #2760 #2783 #2792 #2799

    public class MyAnnotatedService {
        @Get("/sleep/{duration}")
        public void sleep(@Param Duration duration) { ... }
    }
  • You can now determine whether a request was successful or not from HTTP trailers in CircuitBreakerClient and RetryingClient using onResponseTrailers() method. This can be useful when you work with gRPC, whose status code is encoded in the grpc-status trailer. #2816

    CircuitBreaker cb = CircuitBreaker.of("my-service");
    CircuitBreakerRule cbr =
        CircuitBreakerRule.builder()
                          .onResponseTrailers(trailers -> {
                              return trailers.getInt("grpc-status", -1) != 0;
                          })
                          .thenFailure()
                          .build();
    
    MyServiceStub myService =
        Clients.builder("gproto+h2c://example.com/")
               .decorator(CircuitBreakerClient.newDecorator(cb, cbr))
               .build(MyServiceStub.class);
  • RequestLog sanitizers now accept RequestContext as an additional input, so that the sanitizers can behave differently depending on the current path, etc. #2803

    Server server =
        Server.builder()
              .decorator(LoggingService.builder()
                                       .headersSanitizer((ctx, headers) -> {
                                           if (ctx.path().startsWith("/secret/")) {
                                               return "<secret>";
                                           } else {
                                               return headers;
                                           }
                                       })
                                       .newDecorator())
              ...
              .build();
  • Armeria now supports service discovery and registration for Curator Service Discovery and Finagle ServerSets. #2673 #2749 #2791

    CuratorFramework curator = ...;
    // Client side:
    //// Curator Service Discovery:
    EndpointGroup curatorEndpointGroup =
        ZooKeeperEndpointGroup.of(curator, "/discovery/curator",
                                  ZooKeeperDiscoverySpec.curator("my-service"));
    //// Finagle ServerSets:
    EndpointGroup serverSetsEndpointGroup =
        ZooKeeperEndpointGroup.of(curator, "/discovery/serversets"
                                  ZooKeeperDiscoverySpec.serverSets());
    
    // Server-side:
    Server server = ...;
    //// Curator Service Discovery:
    server.addListener(ZooKeeperUpdatingListener.of(
        curator, "/discovery/curator",
        ZooKeeperRegistrationSpec.curator("my-service")));
    //// Finagle ServerSets:
    server.addListener(ZooKeeperUpdatingListener.of(
        curator, "/discovery/serversets",
        ZooKeeperRegistrationSpec.serverSets()));
  • You can now build OAuth1aToken more conveniently using the builder API. #2770

    OAuth1aToken token =
        OAuth1aToken.builder()
                    .realm("...")
                    .consumerKey("...")
                    .token("...")
                    ...
                    .build();
  • The Spring Boot integration now hides all services under /internal/ for non-management ports when management.server.port property is set. #2408 #2502

  • The Spring Boot integration now supports the new graceful shutdown properties introduced in Spring Boot 2.3. #2784 #2802

  • Added a new API for handling reference counted or pooled objects such as PooledHttpData in a relatively safer way. #2448

    • See PooledHttpData, PooledWebClient, PooledHttpRequest and PooledHttpResponse for more information.

📈 Improvements

  • Cleaned up minor issues reported by errorprone. #2772
  • Made some exception messages more user-friendly. #2751

🛠️ Bug fixes

  • It's now allowed to specify an absolute URL only when a WebClient was created without a base URL. #2757
    WebClient clientWithoutBaseUrl = WebClient.of();
    WebClient clientWithBaseUrl = WebClient.of("https://example.com/");
    // Good
    clientWithBaseUrl.get("/bar");
    clientWithoutBaseUrl.get("https://foo.com/");
    // Bad
    clientWithBaseUrl.get("https://foo.com/");
    clientWithoutBaseUrl.get("/bar");
  • Boolean parameter conversion became more strict in annotated services. #2767 #2774
    • Only true, false, 1 and 0 are accepted. Other values will cause a 400 Bad Request response.
  • DocService web UI now shows the 'request body' field for DELETE and PATCH methods in the debug form. #2756 #2819
  • JacksonRequestConverterFunction now handles the case where the target type has a type parameter, e.g. List<Long>. #2769 #2779
  • Fixed a bug where the current ServiceRequestContext is not pushed when invoking ResponseConverterFunction.convertResponse(). #2789
  • RequestContextExportingAppender now handles the <exports /> tag correctly. #2781
  • Fixed a bug where some Reactive Streams Subscriber implementations violate the specification. #2815
  • You no longer get sporadic WriteTimeoutException from proxied connections. #2801 #2805
  • You no longer get a CancelledSubscriptionException unnecessarily when using PublisherBasedStreamMessage. #2797
  • You no longer get sporadic EncoderExceptions from HTTP/1 connections. #2765
  • It's now disallowed to specify the following headers in gRPC Metadata. #2718
    • :status
    • grpc-message
    • grpc-status
    • armeria.grpc.ThrowableProto-bin

🏚️ Deprecations

☢️ Breaking changes

⛓ Dependencies

  • Bouncy Castle 1.65 → 1.65.01
  • Dropwizard 2.0.9 → 2.0.10
  • gRPC 1.29.0 → 1.30.1
  • Jetty 9.4.29 → 9.4.30
  • Reactor 3.3.5 → 3.3.6
  • Spring Boot 2.3.0 → 2.3.1, 2.1.14 → 2.1.15
  • Tomcat 9.0.35 → 9.0.36, 8.5.55 → 8.5.56
  • Example dependencies
    • Dagger 2.27 → 2.28
    • grpc-kotlin-stub 0.1.2 → 0.1.3

🙇 Thank you