From 8c1c0ae1e540c440307b0c99126137b73ae7a04d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 7 Apr 2023 19:30:19 +0000 Subject: [PATCH] Added support for headers for ExtensionRestRequest (#643) * added uri and httpVersion to HttpRequest Signed-off-by: Daulet * added SDKHttpRequest, SDKRestRequest Signed-off-by: Daulet * spotless applied Signed-off-by: Daulet * moved SDKHttpRequest and SDKRestRequest to org.opensearch.sdk.rest Signed-off-by: Daulet * tests changed for new ExtensionRestRequest Signed-off-by: Daulet * TestSDKRestRequest created Signed-off-by: Daulet * corrected test for xContentType, added test for contentParser() Signed-off-by: Daulet * replaced .map() with mapStrings() in contentParser() Signed-off-by: Daulet * Update SDKHttpRequest.java Signed-off-by: Daulet Nassipkali * Fix tests for new format Signed-off-by: Daniel Widdis * Use correct Test annotation so tests actually run Signed-off-by: Daniel Widdis --------- Signed-off-by: Daulet Signed-off-by: Daulet Nassipkali Signed-off-by: Daniel Widdis Co-authored-by: Daniel Widdis (cherry picked from commit dfdb0501bb3e21c7ec858309f90903c4b72160dc) Signed-off-by: github-actions[bot] --- .../ExtensionsRestRequestHandler.java | 86 ++---------- .../opensearch/sdk/rest/SDKHttpRequest.java | 124 ++++++++++++++++++ .../opensearch/sdk/rest/SDKRestRequest.java | 44 +++++++ .../sdk/TestBaseExtensionRestHandler.java | 108 +++------------ .../opensearch/sdk/TestExtensionsRunner.java | 7 +- .../sdk/rest/TestSDKRestRequest.java | 91 +++++++++++++ .../helloworld/rest/TestRestHelloAction.java | 89 +++++++++---- 7 files changed, 359 insertions(+), 190 deletions(-) create mode 100644 src/main/java/org/opensearch/sdk/rest/SDKHttpRequest.java create mode 100644 src/main/java/org/opensearch/sdk/rest/SDKRestRequest.java create mode 100644 src/test/java/org/opensearch/sdk/rest/TestSDKRestRequest.java diff --git a/src/main/java/org/opensearch/sdk/handlers/ExtensionsRestRequestHandler.java b/src/main/java/org/opensearch/sdk/handlers/ExtensionsRestRequestHandler.java index 535910f5..e0e5a741 100644 --- a/src/main/java/org/opensearch/sdk/handlers/ExtensionsRestRequestHandler.java +++ b/src/main/java/org/opensearch/sdk/handlers/ExtensionsRestRequestHandler.java @@ -12,22 +12,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.common.bytes.BytesReference; -import org.opensearch.common.xcontent.XContentType; import org.opensearch.extensions.rest.ExtensionRestRequest; import org.opensearch.extensions.rest.ExtensionRestResponse; import org.opensearch.extensions.rest.RestExecuteOnExtensionResponse; -import org.opensearch.http.HttpRequest; -import org.opensearch.http.HttpResponse; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestRequest.Method; -import org.opensearch.rest.RestStatus; import org.opensearch.sdk.ExtensionRestHandler; import org.opensearch.sdk.ExtensionsRunner; import org.opensearch.sdk.SDKNamedXContentRegistry; - -import java.util.Collections; -import java.util.List; -import java.util.Map; +import org.opensearch.sdk.rest.SDKHttpRequest; +import org.opensearch.sdk.rest.SDKRestRequest; import org.opensearch.sdk.ExtensionRestPathRegistry; @@ -77,73 +69,17 @@ public RestExecuteOnExtensionResponse handleRestExecuteOnExtensionRequest(Extens ); } - // Temporary code to create a RestRequest from the ExtensionRestRequest before header code added - // Remove this and replace with SDKRestRequest being generated by this PR: - // https://github.com/opensearch-project/opensearch-sdk-java/pull/605 - RestRequest restRequest = RestRequest.request(sdkNamedXContentRegistry.getRegistry(), new HttpRequest() { - - @Override - public Method method() { - return request.method(); - } - - @Override - public String uri() { - // path strips query off uri but probably want to pass the whole uri - // this will make the request behave as expected (without query params) - return request.path(); - } - - @Override - public BytesReference content() { - return request.content(); - } - - @Override - public Map> getHeaders() { - // This effectively recreates the only header we need right now - // PR replacing this will pass more headers - XContentType xContentType = request.getXContentType(); - return xContentType == null ? Collections.emptyMap() : Map.of("Content-Type", List.of(xContentType.mediaType())); - } - - @Override - public List strictCookies() { - return Collections.emptyList(); - } - - @Override - public HttpVersion protocolVersion() { - return null; - } - - @Override - public HttpRequest removeHeader(String header) { - // we don't use - return null; - } - - @Override - public HttpResponse createResponse(RestStatus status, BytesReference content) { - return null; - } - - @Override - public Exception getInboundException() { - return null; - } - - @Override - public void release() {} - - @Override - public HttpRequest releaseAndCopy() { - return null; - } - }, null); + SDKRestRequest sdkRestRequest = new SDKRestRequest( + sdkNamedXContentRegistry.getRegistry(), + request.params(), + request.path(), + request.headers(), + new SDKHttpRequest(request), + null + ); // Get response from extension - ExtensionRestResponse response = restHandler.handleRequest(restRequest); + ExtensionRestResponse response = restHandler.handleRequest(sdkRestRequest); logger.info("Sending extension response to OpenSearch: " + response.status()); return new RestExecuteOnExtensionResponse( response.status(), diff --git a/src/main/java/org/opensearch/sdk/rest/SDKHttpRequest.java b/src/main/java/org/opensearch/sdk/rest/SDKHttpRequest.java new file mode 100644 index 00000000..46c8973f --- /dev/null +++ b/src/main/java/org/opensearch/sdk/rest/SDKHttpRequest.java @@ -0,0 +1,124 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.rest; + +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.extensions.rest.ExtensionRestRequest; +import org.opensearch.http.HttpRequest; +import org.opensearch.http.HttpResponse; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestStatus; + +import java.util.List; +import java.util.Map; + +/** + * This class helps to get instance of HttpRequest + */ +public class SDKHttpRequest implements HttpRequest { + private final RestRequest.Method method; + private final String uri; + private final BytesReference content; + private final Map> headers; + private final HttpVersion httpVersion; + + /** + * Instantiates this class with a copy of {@link ExtensionRestRequest} + * + * @param request The request + */ + public SDKHttpRequest(ExtensionRestRequest request) { + this.method = request.method(); + this.uri = request.uri(); + this.content = request.content(); + this.headers = request.headers(); + this.httpVersion = request.protocolVersion(); + } + + @Override + public RestRequest.Method method() { + return method; + } + + @Override + public String uri() { + return uri; + } + + @Override + public BytesReference content() { + return content; + } + + @Override + public Map> getHeaders() { + return headers; + } + + /** + * Not implemented. Does nothing. + * @return null + */ + @Override + public List strictCookies() { + return null; + } + + @Override + public HttpVersion protocolVersion() { + return httpVersion; + } + + /** + * Not implemented. Does nothing. + * @return null + */ + @Override + public HttpRequest removeHeader(String s) { + return null; + } + + /** + * Not implemented. Does nothing. + * @param restStatus response status + * @param bytesReference content + * @return null + */ + @Override + public HttpResponse createResponse(RestStatus restStatus, BytesReference bytesReference) { + return null; + } + + /** + * Not implemented. Does nothing. + * @return null + */ + @Override + public Exception getInboundException() { + return null; + } + + /** + * Not implemented. Does nothing. + */ + @Override + public void release() { + + } + + /** + * Not implemented. Does nothing. + * @return null + */ + @Override + public HttpRequest releaseAndCopy() { + return null; + } +} diff --git a/src/main/java/org/opensearch/sdk/rest/SDKRestRequest.java b/src/main/java/org/opensearch/sdk/rest/SDKRestRequest.java new file mode 100644 index 00000000..0d4187d8 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/rest/SDKRestRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.rest; + +import java.util.List; +import java.util.Map; + +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.http.HttpChannel; +import org.opensearch.http.HttpRequest; +import org.opensearch.rest.RestRequest; + +/** + * This class helps to get instance of RestRequest + */ +public class SDKRestRequest extends RestRequest { + /** + * Instantiates this class with request's params + * + * @param xContentRegistry The request's content registry + * @param params The request's params + * @param path The request's path + * @param headers The request's headers + * @param httpRequest The request's httpRequest + * @param httpChannel The request's http channel + */ + public SDKRestRequest( + NamedXContentRegistry xContentRegistry, + Map params, + String path, + Map> headers, + HttpRequest httpRequest, + HttpChannel httpChannel + ) { + super(xContentRegistry, params, path, headers, httpRequest, httpChannel); + } +} diff --git a/src/test/java/org/opensearch/sdk/TestBaseExtensionRestHandler.java b/src/test/java/org/opensearch/sdk/TestBaseExtensionRestHandler.java index eab8599d..5184ecc1 100644 --- a/src/test/java/org/opensearch/sdk/TestBaseExtensionRestHandler.java +++ b/src/test/java/org/opensearch/sdk/TestBaseExtensionRestHandler.java @@ -12,19 +12,14 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.function.Function; import org.junit.jupiter.api.Test; import org.opensearch.common.bytes.BytesArray; -import org.opensearch.common.bytes.BytesReference; -import org.opensearch.common.xcontent.XContentType; import org.opensearch.extensions.rest.ExtensionRestResponse; -import org.opensearch.http.HttpRequest; -import org.opensearch.http.HttpResponse; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; +import org.opensearch.sdk.rest.TestSDKRestRequest; import org.opensearch.rest.RestStatus; import org.opensearch.test.OpenSearchTestCase; @@ -58,13 +53,16 @@ public void testHandlerDefaultRoutes() { @Test public void testJsonErrorResponse() { - RestRequest successfulRequest = createTestRestRequest( + RestRequest successfulRequest = TestSDKRestRequest.createTestRestRequest( Method.GET, "foo", + "foo", + Collections.emptyMap(), Collections.emptyMap(), null, new BytesArray("bar".getBytes(StandardCharsets.UTF_8)), - "" + "", + null ); ExtensionRestResponse response = handler.handleRequest(successfulRequest); assertEquals(RestStatus.OK, response.status()); @@ -73,13 +71,16 @@ public void testJsonErrorResponse() { @Test public void testErrorResponseOnException() { - RestRequest exceptionalRequest = createTestRestRequest( + RestRequest exceptionalRequest = TestSDKRestRequest.createTestRestRequest( Method.GET, "foo", + "foo", + Collections.emptyMap(), Collections.emptyMap(), null, new BytesArray("baz".getBytes(StandardCharsets.UTF_8)), - "" + "", + null ); ExtensionRestResponse response = handler.handleRequest(exceptionalRequest); assertEquals(RestStatus.INTERNAL_SERVER_ERROR, response.status()); @@ -88,13 +89,16 @@ public void testErrorResponseOnException() { @Test public void testErrorResponseOnUnhandled() { - RestRequest unhandledRequestMethod = createTestRestRequest( + RestRequest unhandledRequestMethod = TestSDKRestRequest.createTestRestRequest( Method.PUT, "foo", + "foo", + Collections.emptyMap(), Collections.emptyMap(), null, new BytesArray(new byte[0]), - "" + "", + null ); ExtensionRestResponse response = handler.handleRequest(unhandledRequestMethod); assertEquals(RestStatus.NOT_FOUND, response.status()); @@ -107,13 +111,16 @@ public void testErrorResponseOnUnhandled() { response.content().utf8ToString() ); - RestRequest unhandledRequestPath = createTestRestRequest( + RestRequest unhandledRequestPath = TestSDKRestRequest.createTestRestRequest( Method.GET, "foobar", + "foobar", + Collections.emptyMap(), Collections.emptyMap(), null, new BytesArray(new byte[0]), - "" + "", + null ); response = handler.handleRequest(unhandledRequestPath); assertEquals(RestStatus.NOT_FOUND, response.status()); @@ -126,77 +133,4 @@ public void testErrorResponseOnUnhandled() { response.content().utf8ToString() ); } - - public static RestRequest createTestRestRequest( - final Method method, - final String path, - final Map params, - final XContentType xContentType, - final BytesReference content, - final String principalIdentifier - ) { - // Temporary code to create a RestRequest from the ExtensionRestRequest before header code added - // Remove this and replace with SDKRestRequest being generated by this PR: - // https://github.com/opensearch-project/opensearch-sdk-java/pull/605 - return RestRequest.request(null, new HttpRequest() { - - @Override - public Method method() { - return method; - } - - @Override - public String uri() { - StringBuilder uri = new StringBuilder(); - for (Entry param : params.entrySet()) { - uri.append(uri.length() == 0 ? '?' : '&').append(param.getKey()).append('=').append(param.getValue()); - } - return path + uri.toString(); - } - - @Override - public BytesReference content() { - return content; - } - - @Override - public Map> getHeaders() { - return xContentType == null ? Collections.emptyMap() : Map.of("Content-Type", List.of(xContentType.mediaType())); - } - - @Override - public List strictCookies() { - return Collections.emptyList(); - } - - @Override - public HttpVersion protocolVersion() { - return null; - } - - @Override - public HttpRequest removeHeader(String header) { - // we don't use - return null; - } - - @Override - public HttpResponse createResponse(RestStatus status, BytesReference content) { - return null; - } - - @Override - public Exception getInboundException() { - return null; - } - - @Override - public void release() {} - - @Override - public HttpRequest releaseAndCopy() { - return null; - } - }, null); - } } diff --git a/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java b/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java index e109f3fa..d6383b70 100644 --- a/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java +++ b/src/test/java/org/opensearch/sdk/TestExtensionsRunner.java @@ -46,6 +46,7 @@ import org.opensearch.extensions.ExtensionDependency; import org.opensearch.extensions.rest.ExtensionRestRequest; import org.opensearch.extensions.rest.RestExecuteOnExtensionResponse; +import org.opensearch.http.HttpRequest; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestStatus; @@ -145,13 +146,17 @@ public void testHandleExtensionRestRequest() throws Exception { String ext = "token_placeholder"; @SuppressWarnings("unused") // placeholder to test the token when identity features merged Principal userPrincipal = () -> "user1"; + HttpRequest.HttpVersion httpVersion = HttpRequest.HttpVersion.HTTP_1_1; ExtensionRestRequest request = new ExtensionRestRequest( Method.GET, "/foo", + "/foo", + Collections.emptyMap(), Collections.emptyMap(), null, new BytesArray("bar"), - ext + ext, + httpVersion ); RestExecuteOnExtensionResponse response = extensionsRestRequestHandler.handleRestExecuteOnExtensionRequest(request); // this will fail in test environment with no registered actions diff --git a/src/test/java/org/opensearch/sdk/rest/TestSDKRestRequest.java b/src/test/java/org/opensearch/sdk/rest/TestSDKRestRequest.java new file mode 100644 index 00000000..48b0b33e --- /dev/null +++ b/src/test/java/org/opensearch/sdk/rest/TestSDKRestRequest.java @@ -0,0 +1,91 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.rest; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.common.bytes.BytesArray; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.extensions.rest.ExtensionRestRequest; +import org.opensearch.http.HttpRequest; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.test.OpenSearchTestCase; + +import static java.util.Map.entry; + +public class TestSDKRestRequest extends OpenSearchTestCase { + @Test + public void testSDKRestRequestMethods() throws IOException { + RestRequest.Method expectedMethod = Method.GET; + String expectedUri = "foobar?foo=bar&baz=42"; + String expectedPath = "foo"; + Map expectedParams = Map.ofEntries(entry("foo", "bar"), entry("baz", "42")); + Map> expectedHeaders = Map.ofEntries( + entry("Content-Type", Arrays.asList("application/json")), + entry("foo", Arrays.asList("hello", "world")) + ); + XContentType exptectedXContentType = XContentType.JSON; + BytesReference expectedContent = new BytesArray("{\"foo\":\"bar\"}"); + + RestRequest sdkRestRequest = createTestRestRequest( + expectedMethod, + expectedUri, + expectedPath, + expectedParams, + expectedHeaders, + exptectedXContentType, + expectedContent, + "", + null + ); + assertEquals(expectedMethod, sdkRestRequest.method()); + assertEquals(expectedUri, sdkRestRequest.uri()); + assertEquals(expectedPath, sdkRestRequest.path()); + assertEquals(expectedParams, sdkRestRequest.params()); + assertEquals(expectedHeaders, sdkRestRequest.getHeaders()); + assertEquals(exptectedXContentType, sdkRestRequest.getXContentType()); + assertEquals(expectedContent, sdkRestRequest.content()); + + Map source = sdkRestRequest.contentParser().mapStrings(); + assertEquals("bar", source.get("foo")); + } + + public static RestRequest createTestRestRequest( + final Method method, + final String uri, + final String path, + final Map params, + final Map> headers, + final XContentType xContentType, + final BytesReference content, + final String principalIdentifier, + final HttpRequest.HttpVersion httpVersion + ) { + // xContentType is not used. It will be parsed from headers + ExtensionRestRequest request = new ExtensionRestRequest( + method, + uri, + path, + params, + headers, + xContentType, + content, + principalIdentifier, + httpVersion + ); + return new SDKRestRequest(null, request.params(), request.path(), request.headers(), new SDKHttpRequest(request), null); + } +} diff --git a/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java b/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java index 08a75ac3..2fcfda5c 100644 --- a/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java +++ b/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java @@ -22,10 +22,11 @@ import org.opensearch.common.bytes.BytesArray; import org.opensearch.common.bytes.BytesReference; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.http.HttpRequest.HttpVersion; import org.opensearch.rest.RestResponse; import org.opensearch.rest.RestStatus; import org.opensearch.sdk.ExtensionRestHandler; -import org.opensearch.sdk.TestBaseExtensionRestHandler; +import org.opensearch.sdk.rest.TestSDKRestRequest; import org.opensearch.test.OpenSearchTestCase; public class TestRestHelloAction extends OpenSearchTestCase { @@ -66,85 +67,115 @@ public void testHandleRequest() { String token = "placeholder_token"; Map params = Collections.emptyMap(); - RestRequest getRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest getRequest = TestSDKRestRequest.createTestRestRequest( Method.GET, "/hello", + "/hello", params, - null, + headers(XContentType.JSON), + XContentType.JSON, new BytesArray(""), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest putRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest putRequest = TestSDKRestRequest.createTestRestRequest( Method.PUT, "/hello/Passing+Test", + "/hello/Passing+Test", Map.of("name", "Passing+Test"), - null, + headers(XContentType.JSON), + XContentType.JSON, new BytesArray(""), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest postRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest postRequest = TestSDKRestRequest.createTestRestRequest( Method.POST, "/hello", + "/hello", params, + headers(XContentType.JSON), XContentType.JSON, new BytesArray("{\"adjective\":\"testable\"}"), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest badPostRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest badPostRequest = TestSDKRestRequest.createTestRestRequest( Method.POST, "/hello", + "/hello", params, + headers(XContentType.JSON), XContentType.JSON, new BytesArray("{\"adjective\":\"\"}"), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest noContentPostRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest noContentPostRequest = TestSDKRestRequest.createTestRestRequest( Method.POST, "/hello", + "/hello", params, + headers(null), null, new BytesArray(""), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest badContentTypePostRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest badContentTypePostRequest = TestSDKRestRequest.createTestRestRequest( Method.POST, "/hello", + "/hello", params, + headers(XContentType.YAML), XContentType.YAML, new BytesArray("yaml:"), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest deleteRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest deleteRequest = TestSDKRestRequest.createTestRestRequest( Method.DELETE, "/goodbye", + "/goodbye", params, - null, + headers(XContentType.JSON), + XContentType.JSON, new BytesArray(""), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest unhandledMethodRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest unhandledMethodRequest = TestSDKRestRequest.createTestRestRequest( Method.HEAD, "/hi", + "/hi", params, - null, + headers(XContentType.JSON), + XContentType.JSON, new BytesArray(""), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest unhandledPathRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest unhandledPathRequest = TestSDKRestRequest.createTestRestRequest( Method.GET, "/hi", + "/hi", params, - null, + headers(XContentType.JSON), + XContentType.JSON, new BytesArray(""), - token + token, + HttpVersion.HTTP_1_1 ); - RestRequest unhandledPathLengthRequest = TestBaseExtensionRestHandler.createTestRestRequest( + RestRequest unhandledPathLengthRequest = TestSDKRestRequest.createTestRestRequest( Method.DELETE, "/goodbye/cruel/world", + "/goodbye/cruel/world", params, - null, + headers(XContentType.JSON), + XContentType.JSON, new BytesArray(""), - token + token, + HttpVersion.HTTP_1_1 ); // Initial default response @@ -235,4 +266,8 @@ public void testHandleRequest() { responseStr = new String(BytesReference.toBytes(response.content()), StandardCharsets.UTF_8); assertTrue(responseStr.contains("/goodbye")); } + + private static Map> headers(XContentType type) { + return type == null ? Collections.emptyMap() : Map.of("Content-Type", List.of(type.mediaType())); + } }