diff --git a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpMessage.java b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpMessage.java index 13a004602..019d8cc54 100644 --- a/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpMessage.java +++ b/http-core/src/main/java/org/apache/pekko/http/javadsl/model/HttpMessage.java @@ -136,10 +136,10 @@ interface MessageTransformations { Self addHeader(HttpHeader header); /** Returns a copy of this message with the given headers added to the list of headers. */ - Self addHeaders(Iterable headers); + Self addHeaders(Iterable headers); /** Returns a copy of this message with new headers. */ - Self withHeaders(Iterable headers); + Self withHeaders(Iterable headers); Self addAttribute(AttributeKey key, T value); diff --git a/http-core/src/main/mima-filters/2.0.x.backwards.excludes/java-iterable-http-header-wildcard.excludes b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/java-iterable-http-header-wildcard.excludes new file mode 100644 index 000000000..dcc27005b --- /dev/null +++ b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/java-iterable-http-header-wildcard.excludes @@ -0,0 +1,26 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Java API collection parameters accept Iterable; only the generic signature changes, the erased signature is unchanged. +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.model.HttpMessage#MessageTransformations.addHeaders(java.lang.Iterable)java.lang.Object") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.model.HttpMessage#MessageTransformations.withHeaders(java.lang.Iterable)java.lang.Object") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.scaladsl.model.HttpMessage.addHeaders(java.lang.Iterable)org.apache.pekko.http.scaladsl.model.HttpMessage") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.scaladsl.model.HttpMessage.withHeaders(java.lang.Iterable)org.apache.pekko.http.scaladsl.model.HttpMessage") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.scaladsl.model.HttpRequest.addHeaders(java.lang.Iterable)org.apache.pekko.http.scaladsl.model.HttpMessage") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.scaladsl.model.HttpRequest.withHeaders(java.lang.Iterable)org.apache.pekko.http.scaladsl.model.HttpMessage") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.scaladsl.model.HttpResponse.addHeaders(java.lang.Iterable)org.apache.pekko.http.scaladsl.model.HttpMessage") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.scaladsl.model.HttpResponse.withHeaders(java.lang.Iterable)org.apache.pekko.http.scaladsl.model.HttpMessage") diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpMessage.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpMessage.scala index a26ebe1b6..39e113ecb 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpMessage.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpMessage.scala @@ -234,12 +234,12 @@ sealed trait HttpMessage extends jm.HttpMessage { } /** Java API */ - def addHeaders(headers: JIterable[jm.HttpHeader]): Self = + def addHeaders(headers: JIterable[? <: jm.HttpHeader]): Self = withHeaders(this.headers ++ headers.asScala.asInstanceOf[Iterable[HttpHeader]]) /** Java API */ - def withHeaders(headers: JIterable[jm.HttpHeader]): Self = - withHeaders(headers.asScala.toVector.map(x => JavaMapping.toScala(x))) + def withHeaders(headers: JIterable[? <: jm.HttpHeader]): Self = + withHeaders(headers.asScala.toVector.map(x => JavaMapping.toScala(x: jm.HttpHeader))) /** Java API */ def getAttribute[T](attributeKey: jm.AttributeKey[T]): Optional[T] = diff --git a/http-core/src/test/java/org/apache/pekko/http/javadsl/model/HttpMessageTest.java b/http-core/src/test/java/org/apache/pekko/http/javadsl/model/HttpMessageTest.java index baf0f8d2f..7409698cb 100644 --- a/http-core/src/test/java/org/apache/pekko/http/javadsl/model/HttpMessageTest.java +++ b/http-core/src/test/java/org/apache/pekko/http/javadsl/model/HttpMessageTest.java @@ -17,8 +17,18 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; +import org.apache.pekko.http.javadsl.model.headers.Location; +import org.apache.pekko.http.javadsl.model.headers.RawHeader; import org.junit.jupiter.api.Test; public class HttpMessageTest { @@ -72,4 +82,100 @@ public void testSslAttribute() throws Exception { assertTrue(request.getAttribute(AttributeKeys.sslSession).get().getSession() == session); } + + @Test + public void testAddAndWithHeadersAcceptIterableOfHeaderSubtype() { + List headers = List.of(RawHeader.create("x-b", "2"), RawHeader.create("x-c", "3")); + + HttpRequest added = + HttpRequest.create().addHeader(RawHeader.create("x-a", "1")).addHeaders(headers); + HttpResponse replaced = + HttpResponse.create().addHeader(RawHeader.create("x-a", "1")).withHeaders(headers); + + assertEquals( + List.of( + RawHeader.create("x-a", "1"), + RawHeader.create("x-b", "2"), + RawHeader.create("x-c", "3")), + added.getHeaders()); + assertEquals(headers, replaced.getHeaders()); + } + + @Test + public void testAddAndWithHeadersAcceptAnyIterable() { + RawHeader a = RawHeader.create("x-a", "1"); + List headers = List.of(RawHeader.create("x-b", "2"), RawHeader.create("x-c", "3")); + + iterablesOf(headers) + .forEach( + (shape, iterable) -> { + HttpRequest addedToRequest = HttpRequest.create().addHeader(a).addHeaders(iterable); + HttpResponse addedToResponse = + HttpResponse.create().addHeader(a).addHeaders(iterable); + HttpRequest replacedOnRequest = + HttpRequest.create().addHeader(a).withHeaders(iterable); + HttpResponse replacedOnResponse = + HttpResponse.create().addHeader(a).withHeaders(iterable); + + List expectedAdded = List.of(a, headers.get(0), headers.get(1)); + assertEquals(expectedAdded, addedToRequest.getHeaders(), "addHeaders(" + shape + ")"); + assertEquals( + expectedAdded, addedToResponse.getHeaders(), "addHeaders(" + shape + ")"); + assertEquals(headers, replacedOnRequest.getHeaders(), "withHeaders(" + shape + ")"); + assertEquals(headers, replacedOnResponse.getHeaders(), "withHeaders(" + shape + ")"); + }); + } + + @Test + public void testAddAndWithHeadersTraverseTheIterableOnce() { + List headers = List.of(RawHeader.create("x-a", "1")); + + assertEquals(headers, HttpRequest.create().addHeaders(oneShot(headers)).getHeaders()); + assertEquals(headers, HttpRequest.create().withHeaders(oneShot(headers)).getHeaders()); + } + + @Test + public void testWithHeadersOfEmptyIterableRemovesAllHeaders() { + HttpRequest request = + HttpRequest.create() + .addHeader(RawHeader.create("x-a", "1")) + .withHeaders(Collections.emptySet()); + + assertEquals(List.of(), request.getHeaders()); + } + + @Test + public void testAddHeadersKeepsDifferentHeaderTypesInOrder() { + Location location = Location.create("/elsewhere"); + RawHeader raw = RawHeader.create("x-a", "1"); + Iterable mixed = new LinkedHashSet<>(List.of(location, raw)); + + HttpResponse response = HttpResponse.create().addHeaders(mixed); + + assertEquals(List.of(location, raw), response.getHeaders()); + assertEquals(location, response.getHeader(Location.class).get()); + } + + /** The same elements as different kinds of {@link Iterable}, keyed by a description. */ + private static Map> iterablesOf(List items) { + Map> shapes = new LinkedHashMap<>(); + shapes.put("ArrayList", new ArrayList<>(items)); + shapes.put("LinkedHashSet", new LinkedHashSet<>(items)); + shapes.put("ArrayDeque", new ArrayDeque<>(items)); + shapes.put("unmodifiable Collection", Collections.unmodifiableCollection(items)); + // not a java.util.Collection at all, only an Iterable + shapes.put("non-Collection Iterable", items::iterator); + return shapes; + } + + /** An {@link Iterable} that fails if it is traversed more than once. */ + private static Iterable oneShot(List items) { + AtomicBoolean traversed = new AtomicBoolean(); + return () -> { + if (traversed.getAndSet(true)) { + throw new IllegalStateException("Iterable traversed more than once"); + } + return items.iterator(); + }; + } } diff --git a/http-tests/src/test/java/org/apache/pekko/http/javadsl/server/directives/RouteDirectivesTest.java b/http-tests/src/test/java/org/apache/pekko/http/javadsl/server/directives/RouteDirectivesTest.java index 3cf5e5025..23184f1a5 100644 --- a/http-tests/src/test/java/org/apache/pekko/http/javadsl/server/directives/RouteDirectivesTest.java +++ b/http-tests/src/test/java/org/apache/pekko/http/javadsl/server/directives/RouteDirectivesTest.java @@ -13,9 +13,20 @@ package org.apache.pekko.http.javadsl.server.directives; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import org.apache.pekko.http.javadsl.model.*; +import org.apache.pekko.http.javadsl.marshalling.Marshaller; import org.apache.pekko.http.javadsl.model.headers.Location; +import org.apache.pekko.http.javadsl.model.headers.RawHeader; import org.apache.pekko.http.javadsl.server.Directives; import org.apache.pekko.http.javadsl.testkit.JUnitJupiterRouteTest; import org.apache.pekko.http.javadsl.testkit.TestRoute; @@ -147,4 +158,90 @@ public void testRouteWhenLambdaThrows() { route.run(HttpRequest.create("/foo")).assertEntity("There was an internal server error."); } + + @Test + public void testCompleteAcceptsAnyIterableOfHeaderSubtype() { + List headers = List.of(RawHeader.create("x-a", "1"), RawHeader.create("x-b", "2")); + + iterablesOf(headers) + .forEach( + (shape, iterable) -> + assertDoesNotThrow( + () -> assertCompleteOverloadsRespondWithHeaders(iterable), + "complete/entityToResponse with " + shape)); + } + + @Test + public void testCompleteWithDifferentHeaderTypes() { + Location location = Location.create("/elsewhere"); + List headers = List.of(location, RawHeader.create("x-a", "1")); + + testRoute(complete(StatusCodes.CREATED, headers, HttpEntities.create("created"))) + .run(HttpRequest.GET("/")) + .assertStatusCode(StatusCodes.CREATED) + .assertHeaderExists(location) + .assertHeaderExists("x-a", "1") + .assertEntity("created"); + } + + private void assertCompleteOverloadsRespondWithHeaders(Iterable headers) { + ResponseEntity responseEntity = HttpEntities.create("response"); + RequestEntity requestEntity = HttpEntities.create("request"); + + TestRoute route = + testRoute( + path( + "status-value", + () -> + complete(StatusCodes.ACCEPTED, headers, "value", Marshaller.stringToEntity())), + path("status-response", () -> complete(StatusCodes.ACCEPTED, headers, responseEntity)), + path("status-request", () -> complete(StatusCodes.ACCEPTED, headers, requestEntity)), + path("value", () -> complete(headers, "value", Marshaller.stringToEntity())), + path("response", () -> complete(headers, responseEntity)), + path("request", () -> complete(headers, requestEntity)), + path( + "marshaller", + () -> + complete( + "value", + Marshaller.entityToResponse( + StatusCodes.ACCEPTED, headers, Marshaller.stringToEntity()))), + path( + "ok-marshaller", + () -> + complete( + "value", + Marshaller.entityToOKResponse(headers, Marshaller.stringToEntity())))); + + String[][] expectations = { + {"status-value", "202", "value"}, + {"status-response", "202", "response"}, + {"status-request", "202", "request"}, + {"value", "200", "value"}, + {"response", "200", "response"}, + {"request", "200", "request"}, + {"marshaller", "202", "value"}, + {"ok-marshaller", "200", "value"} + }; + for (String[] e : expectations) { + route + .run(HttpRequest.GET("/" + e[0])) + .assertStatusCode(Integer.parseInt(e[1])) + .assertHeaderExists("x-a", "1") + .assertHeaderExists("x-b", "2") + .assertEntity(e[2]); + } + } + + /** The same elements as different kinds of {@link Iterable}, keyed by a description. */ + private static Map> iterablesOf(List items) { + Map> shapes = new LinkedHashMap<>(); + shapes.put("ArrayList", new ArrayList<>(items)); + shapes.put("LinkedHashSet", new LinkedHashSet<>(items)); + shapes.put("ArrayDeque", new ArrayDeque<>(items)); + shapes.put("unmodifiable Collection", Collections.unmodifiableCollection(items)); + // not a java.util.Collection at all, only an Iterable + shapes.put("non-Collection Iterable", items::iterator); + return shapes; + } } diff --git a/http/src/main/mima-filters/2.0.x.backwards.excludes/java-iterable-http-header-wildcard.excludes b/http/src/main/mima-filters/2.0.x.backwards.excludes/java-iterable-http-header-wildcard.excludes new file mode 100644 index 000000000..b4123881f --- /dev/null +++ b/http/src/main/mima-filters/2.0.x.backwards.excludes/java-iterable-http-header-wildcard.excludes @@ -0,0 +1,26 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Java API collection parameters accept Iterable; only the generic signature changes, the erased signature is unchanged. +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.marshalling.Marshaller.entityToOKResponse(java.lang.Iterable,org.apache.pekko.http.javadsl.marshalling.Marshaller)org.apache.pekko.http.javadsl.marshalling.Marshaller") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.marshalling.Marshaller.entityToResponse(org.apache.pekko.http.javadsl.model.StatusCode,java.lang.Iterable,org.apache.pekko.http.javadsl.marshalling.Marshaller)org.apache.pekko.http.javadsl.marshalling.Marshaller") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.server.directives.RouteDirectives.complete(java.lang.Iterable,java.lang.Object,org.apache.pekko.http.javadsl.marshalling.Marshaller)org.apache.pekko.http.javadsl.server.directives.RouteAdapter") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.server.directives.RouteDirectives.complete(java.lang.Iterable,org.apache.pekko.http.javadsl.model.RequestEntity)org.apache.pekko.http.javadsl.server.directives.RouteAdapter") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.server.directives.RouteDirectives.complete(java.lang.Iterable,org.apache.pekko.http.javadsl.model.ResponseEntity)org.apache.pekko.http.javadsl.server.directives.RouteAdapter") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.server.directives.RouteDirectives.complete(org.apache.pekko.http.javadsl.model.StatusCode,java.lang.Iterable,java.lang.Object,org.apache.pekko.http.javadsl.marshalling.Marshaller)org.apache.pekko.http.javadsl.server.directives.RouteAdapter") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.server.directives.RouteDirectives.complete(org.apache.pekko.http.javadsl.model.StatusCode,java.lang.Iterable,org.apache.pekko.http.javadsl.model.RequestEntity)org.apache.pekko.http.javadsl.server.directives.RouteAdapter") +ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.javadsl.server.directives.RouteDirectives.complete(org.apache.pekko.http.javadsl.model.StatusCode,java.lang.Iterable,org.apache.pekko.http.javadsl.model.ResponseEntity)org.apache.pekko.http.javadsl.server.directives.RouteAdapter") diff --git a/http/src/main/scala/org/apache/pekko/http/javadsl/marshalling/Marshaller.scala b/http/src/main/scala/org/apache/pekko/http/javadsl/marshalling/Marshaller.scala index b09514fa9..f06a8df7a 100644 --- a/http/src/main/scala/org/apache/pekko/http/javadsl/marshalling/Marshaller.scala +++ b/http/src/main/scala/org/apache/pekko/http/javadsl/marshalling/Marshaller.scala @@ -32,6 +32,10 @@ object Marshaller { import JavaMapping.Implicits._ + // java.lang.Iterable is read-only, so widening the element type is safe + private def headersAsScala(headers: java.lang.Iterable[? <: HttpHeader]) = + headers.asInstanceOf[java.lang.Iterable[HttpHeader]].asScala + def fromScala[A, B](scalaMarshaller: marshalling.Marshaller[A, B]): Marshaller[A, B] = new Marshaller()(scalaMarshaller) @@ -97,14 +101,16 @@ object Marshaller { fromScala(marshalling.Marshaller.fromToEntityMarshaller[A](status.asScala)(m.asScalaCastOutput)) } - def entityToResponse[A](status: StatusCode, headers: java.lang.Iterable[HttpHeader], + def entityToResponse[A](status: StatusCode, headers: java.lang.Iterable[? <: HttpHeader], m: Marshaller[A, ? <: RequestEntity]): Marshaller[A, HttpResponse] = { - fromScala(marshalling.Marshaller.fromToEntityMarshaller[A](status.asScala, headers.asScala)(m.asScalaCastOutput)) + fromScala(marshalling.Marshaller.fromToEntityMarshaller[A](status.asScala, headersAsScala(headers))( + m.asScalaCastOutput)) } def entityToOKResponse[A]( - headers: java.lang.Iterable[HttpHeader], m: Marshaller[A, ? <: RequestEntity]): Marshaller[A, HttpResponse] = { - fromScala(marshalling.Marshaller.fromToEntityMarshaller[A](headers = headers.asScala)(m.asScalaCastOutput)) + headers: java.lang.Iterable[? <: HttpHeader], m: Marshaller[A, ? <: RequestEntity]) + : Marshaller[A, HttpResponse] = { + fromScala(marshalling.Marshaller.fromToEntityMarshaller[A](headers = headersAsScala(headers))(m.asScalaCastOutput)) } // these are methods not varargs to avoid call site warning about unchecked type params diff --git a/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/RouteDirectives.scala b/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/RouteDirectives.scala index fc8d2832c..58f396a46 100644 --- a/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/RouteDirectives.scala +++ b/http/src/main/scala/org/apache/pekko/http/javadsl/server/directives/RouteDirectives.scala @@ -117,22 +117,26 @@ abstract class RouteDirectives extends RespondWithDirectives { /** * Completes the request using the given status code and headers, marshalling the given value as response entity. */ - def complete[T](status: StatusCode, headers: java.lang.Iterable[HttpHeader], value: T, + def complete[T](status: StatusCode, headers: java.lang.Iterable[? <: HttpHeader], value: T, marshaller: Marshaller[T, RequestEntity]) = RouteAdapter { - D.complete(ToResponseMarshallable(value)(fromToEntityMarshaller(status.asScala, headers.asScala)(marshaller))) + D.complete(ToResponseMarshallable(value)(fromToEntityMarshaller(status.asScala, headersAsScala(headers))( + marshaller))) } /** * Completes the request using the given status code, headers, and response entity. */ - def complete(status: StatusCode, headers: java.lang.Iterable[HttpHeader], entity: ResponseEntity) = RouteAdapter { - D.complete(scaladsl.model.HttpResponse(status = status.asScala, entity = entity.asScala, headers = headers.asScala)) - } + def complete(status: StatusCode, headers: java.lang.Iterable[? <: HttpHeader], entity: ResponseEntity) = + RouteAdapter { + D.complete(scaladsl.model.HttpResponse(status = status.asScala, entity = entity.asScala, + headers = headersAsScala(headers))) + } /** * Completes the request using the given status code, headers, and response entity. */ - def complete(status: StatusCode, headers: java.lang.Iterable[HttpHeader], entity: RequestEntity): RouteAdapter = { + def complete(status: StatusCode, headers: java.lang.Iterable[? <: HttpHeader], entity: RequestEntity) + : RouteAdapter = { complete(status, headers, entity: ResponseEntity) } @@ -165,22 +169,22 @@ abstract class RouteDirectives extends RespondWithDirectives { /** * Completes the request as HTTP 200 OK, adding the given headers, and marshalling the given value as response entity. */ - def complete[T](headers: java.lang.Iterable[HttpHeader], value: T, marshaller: Marshaller[T, RequestEntity]) = + def complete[T](headers: java.lang.Iterable[? <: HttpHeader], value: T, marshaller: Marshaller[T, RequestEntity]) = RouteAdapter { - D.complete(ToResponseMarshallable(value)(fromToEntityMarshaller(headers = headers.asScala)(marshaller))) + D.complete(ToResponseMarshallable(value)(fromToEntityMarshaller(headers = headersAsScala(headers))(marshaller))) } /** * Completes the request as HTTP 200 OK, adding the given headers and response entity. */ - def complete(headers: java.lang.Iterable[HttpHeader], entity: ResponseEntity) = RouteAdapter { - D.complete(scaladsl.model.HttpResponse(headers = headers.asScala, entity = entity.asScala)) + def complete(headers: java.lang.Iterable[? <: HttpHeader], entity: ResponseEntity) = RouteAdapter { + D.complete(scaladsl.model.HttpResponse(headers = headersAsScala(headers), entity = entity.asScala)) } /** * Completes the request as HTTP 200 OK, adding the given headers and response entity. */ - def complete(headers: java.lang.Iterable[HttpHeader], entity: RequestEntity): RouteAdapter = + def complete(headers: java.lang.Iterable[? <: HttpHeader], entity: RequestEntity): RouteAdapter = complete(headers, entity: ResponseEntity) /** @@ -318,6 +322,10 @@ abstract class RouteDirectives extends RespondWithDirectives { // Right now, having this in Java: // CompletableFuture.supplyAsync(() -> { throw new IllegalArgumentException("always failing"); }) // will in fact fail the future with CompletionException. + // java.lang.Iterable is read-only, so widening the element type is safe + private def headersAsScala(headers: java.lang.Iterable[? <: HttpHeader]) = + headers.asInstanceOf[java.lang.Iterable[HttpHeader]].asScala + private def unwrapCompletionException[T]: PartialFunction[Throwable, T] = { case x: CompletionException if x.getCause ne null => throw x.getCause