diff --git a/invoker/core/pom.xml b/invoker/core/pom.xml
index 8108c526..d1a94d3d 100644
--- a/invoker/core/pom.xml
+++ b/invoker/core/pom.xml
@@ -23,6 +23,7 @@
17
17
4.0.1
+ 12.1.3
@@ -46,11 +47,6 @@
functions-framework-api
1.1.4
-
- javax.servlet
- javax.servlet-api
- 4.0.1
-
io.cloudevents
cloudevents-core
@@ -97,13 +93,13 @@
org.eclipse.jetty
- jetty-servlet
- 9.4.58.v20250814
+ jetty-server
+ ${jetty.version}
- org.eclipse.jetty
- jetty-server
- 9.4.58.v20250814
+ org.slf4j
+ slf4j-jdk14
+ 2.0.9
com.beust
@@ -151,7 +147,7 @@
org.eclipse.jetty
jetty-client
- 9.4.58.v20250814
+ ${jetty.version}
test
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java
index 331c9586..097b9a67 100644
--- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java
@@ -30,25 +30,32 @@
import io.cloudevents.http.HttpMessageFactory;
import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
/** Executes the user's background function. */
-public final class BackgroundFunctionExecutor extends HttpServlet {
+public final class BackgroundFunctionExecutor extends Handler.Abstract {
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
private final FunctionExecutor> functionExecutor;
@@ -177,8 +184,13 @@ static Optional backgroundFunctionTypeArgument(
.findFirst();
}
- private static Event parseLegacyEvent(HttpServletRequest req) throws IOException {
- try (BufferedReader bodyReader = req.getReader()) {
+ private static Event parseLegacyEvent(Request req) throws IOException {
+ try (BufferedReader bodyReader =
+ new BufferedReader(
+ new InputStreamReader(
+ Content.Source.asInputStream(req),
+ Objects.requireNonNullElse(
+ Request.getCharset(req), StandardCharsets.ISO_8859_1)))) {
return parseLegacyEvent(bodyReader);
}
}
@@ -225,7 +237,7 @@ private static Context contextFromCloudEvent(CloudEvent cloudEvent) {
* for the various triggers. CloudEvents are ones that follow the standards defined by cloudevents.io.
*
- * @param the type to be used in the {@link Unmarshallers} call when
+ * @param the type to be used in the {code Unmarshallers} call when
* unmarshalling this event, if it is a CloudEvent.
*/
private abstract static class FunctionExecutor {
@@ -322,23 +334,25 @@ void serviceCloudEvent(CloudEvent cloudEvent) throws Exception {
/** Executes the user's background function. This can handle all HTTP methods. */
@Override
- public void service(HttpServletRequest req, HttpServletResponse res) throws IOException {
- String contentType = req.getContentType();
+ public boolean handle(Request req, Response res, Callback callback) throws Exception {
+ String contentType = req.getHeaders().get(HttpHeader.CONTENT_TYPE);
try {
executionIdUtil.storeExecutionId(req);
if ((contentType != null && contentType.startsWith("application/cloudevents+json"))
- || req.getHeader("ce-specversion") != null) {
+ || req.getHeaders().get("ce-specversion") != null) {
serviceCloudEvent(req);
} else {
serviceLegacyEvent(req);
}
- res.setStatus(HttpServletResponse.SC_OK);
+ res.setStatus(HttpStatus.OK_200);
+ callback.succeeded();
} catch (Throwable t) {
- res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
logger.log(Level.SEVERE, "Failed to execute " + functionExecutor.functionName(), t);
+ Response.writeError(req, res, callback, HttpStatus.INTERNAL_SERVER_ERROR_500, null);
} finally {
executionIdUtil.removeExecutionId();
}
+ return true;
}
private enum CloudEventKind {
@@ -352,10 +366,14 @@ private enum CloudEventKind {
* @param a fake type parameter, which corresponds to the type parameter of {@link
* FunctionExecutor}.
*/
- private void serviceCloudEvent(HttpServletRequest req) throws Exception {
+ private void serviceCloudEvent(Request req) throws Exception {
@SuppressWarnings("unchecked")
FunctionExecutor executor = (FunctionExecutor) functionExecutor;
- byte[] body = req.getInputStream().readAllBytes();
+
+ // Read the entire request body into a byte array.
+ // TODO: this method is deprecated for removal, use the method introduced by
+ // https://github.com/jetty/jetty.project/pull/13939 when it is released.
+ byte[] body = Content.Source.asByteArrayAsync(req, -1).get();
MessageReader reader = HttpMessageFactory.createReaderFromMultimap(headerMap(req), body);
// It's important not to set the context ClassLoader earlier, because MessageUtils will use
// ServiceLoader.load(EventFormat.class) to find a handler to deserialize a binary CloudEvent
@@ -369,17 +387,17 @@ private void serviceCloudEvent(HttpServletRequest req) throws Exce
// https://github.com/cloudevents/sdk-java/pull/259.
}
- private static Map> headerMap(HttpServletRequest req) {
+ private static Map> headerMap(Request req) {
Map> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- for (String header : Collections.list(req.getHeaderNames())) {
- for (String value : Collections.list(req.getHeaders(header))) {
- headerMap.computeIfAbsent(header, unused -> new ArrayList<>()).add(value);
- }
+ for (HttpField field : req.getHeaders()) {
+ headerMap
+ .computeIfAbsent(field.getName(), unused -> new ArrayList<>())
+ .addAll(field.getValueList());
}
return headerMap;
}
- private void serviceLegacyEvent(HttpServletRequest req) throws Exception {
+ private void serviceLegacyEvent(Request req) throws Exception {
Event event = parseLegacyEvent(req);
runWithContextClassLoader(() -> functionExecutor.serviceLegacyEvent(event));
}
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java
index 01f07e74..b414f110 100644
--- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java
@@ -20,12 +20,14 @@
import com.google.cloud.functions.invoker.http.HttpResponseImpl;
import java.util.logging.Level;
import java.util.logging.Logger;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
/** Executes the user's method. */
-public class HttpFunctionExecutor extends HttpServlet {
+public class HttpFunctionExecutor extends Handler.Abstract {
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
private final HttpFunction function;
@@ -65,21 +67,23 @@ public static HttpFunctionExecutor forClass(Class> functionClass) {
/** Executes the user's method, can handle all HTTP type methods. */
@Override
- public void service(HttpServletRequest req, HttpServletResponse res) {
- HttpRequestImpl reqImpl = new HttpRequestImpl(req);
- HttpResponseImpl respImpl = new HttpResponseImpl(res);
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+
+ HttpRequestImpl reqImpl = new HttpRequestImpl(request);
+ HttpResponseImpl respImpl = new HttpResponseImpl(response);
ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader();
try {
- executionIdUtil.storeExecutionId(req);
+ executionIdUtil.storeExecutionId(request);
Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader());
function.service(reqImpl, respImpl);
+ respImpl.close(callback);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t);
- res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500, null);
} finally {
Thread.currentThread().setContextClassLoader(oldContextLoader);
executionIdUtil.removeExecutionId();
- respImpl.flush();
}
+ return true;
}
}
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/TypedFunctionExecutor.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/TypedFunctionExecutor.java
index a6edfc32..63418705 100644
--- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/TypedFunctionExecutor.java
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/TypedFunctionExecutor.java
@@ -15,11 +15,13 @@
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
-public class TypedFunctionExecutor extends HttpServlet {
+public class TypedFunctionExecutor extends Handler.Abstract {
private static final String APPLY_METHOD = "apply";
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
@@ -94,7 +96,7 @@ static Optional handlerTypeArgument(Class extends TypedFunction, ?>> f
/** Executes the user's method, can handle all HTTP type methods. */
@Override
- public void service(HttpServletRequest req, HttpServletResponse res) {
+ public boolean handle(Request req, Response res, Callback callback) throws Exception {
HttpRequestImpl reqImpl = new HttpRequestImpl(req);
HttpResponseImpl resImpl = new HttpResponseImpl(res);
ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();
@@ -102,10 +104,13 @@ public void service(HttpServletRequest req, HttpServletResponse res) {
try {
Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader());
handleRequest(reqImpl, resImpl);
+ resImpl.close(callback);
+ } catch (Throwable t) {
+ Response.writeError(req, res, callback, HttpStatus.INTERNAL_SERVER_ERROR_500, null, t);
} finally {
Thread.currentThread().setContextClassLoader(oldContextClassLoader);
- resImpl.flush();
}
+ return true;
}
private void handleRequest(HttpRequest req, HttpResponse res) {
@@ -114,7 +119,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
reqObj = format.deserialize(req, argType);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to parse request for " + function.getClass().getName(), t);
- res.setStatusCode(HttpServletResponse.SC_BAD_REQUEST);
+ res.setStatusCode(HttpStatus.BAD_REQUEST_400);
return;
}
@@ -123,7 +128,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
resObj = function.apply(reqObj);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t);
- res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ res.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
return;
}
@@ -132,7 +137,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
} catch (Throwable t) {
logger.log(
Level.SEVERE, "Failed to serialize response for " + function.getClass().getName(), t);
- res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ res.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
return;
}
}
@@ -147,7 +152,7 @@ private static class GsonWireFormat implements TypedFunction.WireFormat {
@Override
public void serialize(Object object, HttpResponse response) throws Exception {
if (object == null) {
- response.setStatusCode(HttpServletResponse.SC_NO_CONTENT);
+ response.setStatusCode(HttpStatus.NO_CONTENT_204);
return;
}
try (BufferedWriter bodyWriter = response.getWriter()) {
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/ExecutionIdUtil.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/ExecutionIdUtil.java
index 7987317d..becf2c5c 100644
--- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/ExecutionIdUtil.java
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/ExecutionIdUtil.java
@@ -5,7 +5,7 @@
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Handler;
import java.util.logging.Logger;
-import javax.servlet.http.HttpServletRequest;
+import org.eclipse.jetty.server.Request;
/**
* A helper class that either fetches a unique execution id from request HTTP headers or generates a
@@ -23,7 +23,7 @@ public final class ExecutionIdUtil {
* Add mapping to root logger from current thread id to execution id. This mapping will be used to
* append the execution id to log lines.
*/
- public void storeExecutionId(HttpServletRequest request) {
+ public void storeExecutionId(Request request) {
if (!executionIdLoggingEnabled()) {
return;
}
@@ -47,8 +47,8 @@ public void removeExecutionId() {
}
}
- private String getOrGenerateExecutionId(HttpServletRequest request) {
- String executionId = request.getHeader(EXECUTION_ID_HTTP_HEADER);
+ private String getOrGenerateExecutionId(Request request) {
+ String executionId = request.getHeaders().get(EXECUTION_ID_HTTP_HEADER);
if (executionId == null) {
byte[] array = new byte[EXECUTION_ID_LENGTH];
random.nextBytes(array);
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java
index 2119645a..31ee4ac6 100644
--- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java
@@ -14,33 +14,33 @@
package com.google.cloud.functions.invoker.http;
-import static java.util.stream.Collectors.toMap;
-
import com.google.cloud.functions.HttpRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.io.UncheckedIOException;
-import java.util.AbstractMap.SimpleEntry;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
-import java.util.TreeMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.Part;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.MultiPart.Part;
+import org.eclipse.jetty.http.MultiPartFormData;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.Fields;
public class HttpRequestImpl implements HttpRequest {
- private final HttpServletRequest request;
+ private final Request request;
+ private InputStream inputStream;
+ private BufferedReader reader;
- public HttpRequestImpl(HttpServletRequest request) {
+ public HttpRequestImpl(Request request) {
this.request = request;
}
@@ -51,133 +51,155 @@ public String getMethod() {
@Override
public String getUri() {
- String url = request.getRequestURL().toString();
- if (request.getQueryString() != null) {
- url += "?" + request.getQueryString();
- }
- return url;
+ return request.getHttpURI().asString();
}
@Override
public String getPath() {
- return request.getRequestURI();
+ return request.getHttpURI().getCanonicalPath();
}
@Override
public Optional getQuery() {
- return Optional.ofNullable(request.getQueryString());
+ return Optional.ofNullable(request.getHttpURI().getQuery());
}
@Override
public Map> getQueryParameters() {
- return request.getParameterMap().entrySet().stream()
- .collect(toMap(Map.Entry::getKey, e -> Arrays.asList(e.getValue())));
+ Fields fields = Request.extractQueryParameters(request);
+ if (fields.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ Map> map = new HashMap<>();
+ fields.forEach(
+ field -> map.put(field.getName(), Collections.unmodifiableList(field.getValues())));
+ return Collections.unmodifiableMap(map);
}
@Override
public Map getParts() {
- String contentType = request.getContentType();
- if (contentType == null || !request.getContentType().startsWith("multipart/form-data")) {
+ String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
+ if (contentType == null
+ || !contentType.startsWith(MimeTypes.Type.MULTIPART_FORM_DATA.asString())) {
throw new IllegalStateException("Content-Type must be multipart/form-data: " + contentType);
}
- try {
- return request.getParts().stream().collect(toMap(Part::getName, HttpPartImpl::new));
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- } catch (ServletException e) {
- throw new RuntimeException(e.getMessage(), e);
+
+ // The multipart parsing is done by the EagerContentHandler, so we just call getParts.
+ MultiPartFormData.Parts parts = MultiPartFormData.getParts(request);
+ if (parts == null) {
+ throw new IllegalStateException();
+ }
+
+ if (parts.size() == 0) {
+ return Collections.emptyMap();
}
+
+ Map map = new HashMap<>();
+ parts.forEach(part -> map.put(part.getName(), new HttpPartImpl(part)));
+ return Collections.unmodifiableMap(map);
}
@Override
public Optional getContentType() {
- return Optional.ofNullable(request.getContentType());
+ return Optional.ofNullable(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
}
@Override
public long getContentLength() {
- return request.getContentLength();
+ return request.getLength();
}
@Override
public Optional getCharacterEncoding() {
- return Optional.ofNullable(request.getCharacterEncoding());
+ Charset charset = Request.getCharset(request);
+ return Optional.ofNullable(charset == null ? null : charset.name());
}
@Override
public InputStream getInputStream() throws IOException {
- return request.getInputStream();
+ if (reader != null) {
+ throw new IllegalStateException("getReader() already called");
+ }
+ if (inputStream == null) {
+ inputStream = Content.Source.asInputStream(request);
+ }
+ return inputStream;
}
@Override
public BufferedReader getReader() throws IOException {
- return request.getReader();
+ if (reader == null) {
+ if (inputStream != null) {
+ throw new IllegalStateException("getInputStream already called");
+ }
+ inputStream = Content.Source.asInputStream(request);
+ reader =
+ new BufferedReader(
+ new InputStreamReader(
+ getInputStream(),
+ Objects.requireNonNullElse(Request.getCharset(request), StandardCharsets.UTF_8)));
+ }
+ return reader;
}
@Override
public Map> getHeaders() {
- return Collections.list(request.getHeaderNames()).stream()
- .collect(
- toMap(
- name -> name,
- name -> Collections.list(request.getHeaders(name)),
- (a, b) -> b,
- () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
+ return HttpUtil.toStringListMap(request.getHeaders());
}
private static class HttpPartImpl implements HttpPart {
private final Part part;
+ private final String contentType;
private HttpPartImpl(Part part) {
this.part = part;
+ contentType = part.getHeaders().get(HttpHeader.CONTENT_TYPE);
}
@Override
public Optional getFileName() {
- return Optional.ofNullable(part.getSubmittedFileName());
+ return Optional.ofNullable(part.getFileName());
}
@Override
public Optional getContentType() {
- return Optional.ofNullable(part.getContentType());
+ return Optional.ofNullable(contentType);
}
@Override
public long getContentLength() {
- return part.getSize();
+ return part.getLength();
}
@Override
public Optional getCharacterEncoding() {
- String contentType = getContentType().orElse(null);
- if (contentType == null) {
- return Optional.empty();
- }
- Pattern charsetPattern = Pattern.compile("(?i).*;\\s*charset\\s*=([^;\\s]*)\\s*(;|$)");
- Matcher matcher = charsetPattern.matcher(contentType);
- return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty();
+ return Optional.ofNullable(MimeTypes.getCharsetFromContentType(contentType));
}
@Override
public InputStream getInputStream() throws IOException {
- return part.getInputStream();
+ Content.Source contentSource = part.createContentSource();
+ return Content.Source.asInputStream(contentSource);
}
@Override
public BufferedReader getReader() throws IOException {
- String encoding = getCharacterEncoding().orElse("utf-8");
- return new BufferedReader(new InputStreamReader(getInputStream(), encoding));
+ return new BufferedReader(
+ new InputStreamReader(
+ getInputStream(),
+ Objects.requireNonNullElse(
+ MimeTypes.DEFAULTS.getCharset(contentType), StandardCharsets.UTF_8)));
}
@Override
public Map> getHeaders() {
- return part.getHeaderNames().stream()
- .map(name -> new SimpleEntry<>(name, list(part.getHeaders(name))))
- .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
+ return HttpUtil.toStringListMap(part.getHeaders());
}
- private static List list(Collection collection) {
- return (collection instanceof List>) ? (List) collection : new ArrayList<>(collection);
+ @Override
+ public String toString() {
+ return "%s{%s}".formatted(super.toString(), part);
}
}
}
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java
index c02246f0..088738f9 100644
--- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java
@@ -14,24 +14,33 @@
package com.google.cloud.functions.invoker.http;
-import static java.util.stream.Collectors.toMap;
-
import com.google.cloud.functions.HttpResponse;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Collection;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
-import java.util.TreeMap;
-import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.io.WriteThroughWriter;
+import org.eclipse.jetty.io.content.BufferedContentSink;
+import org.eclipse.jetty.io.content.ContentSinkOutputStream;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
public class HttpResponseImpl implements HttpResponse {
- private final HttpServletResponse response;
+ private final Response response;
+ private ContentSinkOutputStream outputStream;
+ private BufferedWriter writer;
+ private Charset charset;
- public HttpResponseImpl(HttpServletResponse response) {
+ public HttpResponseImpl(Response response) {
this.response = response;
}
@@ -43,75 +52,157 @@ public void setStatusCode(int code) {
@Override
@SuppressWarnings("deprecation")
public void setStatusCode(int code, String message) {
- response.setStatus(code, message);
+ response.setStatus(code);
}
@Override
public void setContentType(String contentType) {
- response.setContentType(contentType);
+ response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType);
+ charset = response.getRequest().getContext().getMimeTypes().getCharset(contentType);
}
@Override
public Optional getContentType() {
- return Optional.ofNullable(response.getContentType());
+ return Optional.ofNullable(response.getHeaders().get(HttpHeader.CONTENT_TYPE));
}
@Override
public void appendHeader(String key, String value) {
- response.addHeader(key, value);
+ if (HttpHeader.CONTENT_TYPE.is(key)) {
+ setContentType(value);
+ } else {
+ response.getHeaders().add(key, value);
+ }
}
@Override
public Map> getHeaders() {
- return response.getHeaderNames().stream()
- .collect(
- toMap(
- name -> name,
- name -> new ArrayList<>(response.getHeaders(name)),
- (a, b) -> b,
- () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
- }
-
- private static List list(Collection collection) {
- return (collection instanceof List>) ? (List) collection : new ArrayList<>(collection);
+ return HttpUtil.toStringListMap(response.getHeaders());
}
@Override
- public OutputStream getOutputStream() throws IOException {
- return response.getOutputStream();
+ public OutputStream getOutputStream() {
+ if (writer != null) {
+ throw new IllegalStateException("getWriter called");
+ }
+ if (outputStream == null) {
+ Request request = response.getRequest();
+ int outputBufferSize =
+ request.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize();
+ BufferedContentSink bufferedContentSink =
+ new BufferedContentSink(
+ response,
+ request.getComponents().getByteBufferPool(),
+ false,
+ outputBufferSize / 2,
+ outputBufferSize);
+ outputStream = new ContentSinkOutputStream(bufferedContentSink);
+ }
+ return outputStream;
}
- private BufferedWriter writer;
-
@Override
public synchronized BufferedWriter getWriter() throws IOException {
if (writer == null) {
- // Unfortunately this means that we get two intermediate objects between the object we return
- // and the underlying Writer that response.getWriter() wraps. We could try accessing the
- // PrintWriter.out field via reflection, but that sort of access to non-public fields of
- // platform classes is now frowned on and may draw warnings or even fail in subsequent
- // versions. We could instead wrap the OutputStream, but that would require us to deduce the
- // appropriate Charset, using logic like this:
- // https://github.com/eclipse/jetty.project/blob/923ec38adf/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java#L731
- // We may end up doing that if performance is an issue.
- writer = new BufferedWriter(response.getWriter());
+ if (outputStream != null) {
+ throw new IllegalStateException("getOutputStream called");
+ }
+
+ writer =
+ new NonBufferedWriter(
+ WriteThroughWriter.newWriter(
+ getOutputStream(), Objects.requireNonNullElse(charset, StandardCharsets.UTF_8)));
}
return writer;
}
- public void flush() {
+ /**
+ * Close the response, flushing all content.
+ *
+ * @param callback a {@link Callback} to be completed when the response is closed.
+ */
+ public void close(Callback callback) {
try {
- // We can't use HttpServletResponse.flushBuffer() because we wrap the
- // PrintWriter returned by HttpServletResponse in our own BufferedWriter
- // to match our API. So we have to flush whichever of getWriter() or
- // getOutputStream() works.
- try {
- getOutputStream().flush();
- } catch (IllegalStateException e) {
- getWriter().flush();
+ // The writer has been constructed to do no buffering, so it does not need to be flushed
+ if (outputStream != null) {
+ // Do an asynchronous close, so large buffered content may be written without blocking
+ outputStream.close(callback);
+ } else {
+ callback.succeeded();
}
} catch (IOException e) {
- // Too bad, can't flush.
+ // Too bad, can't close.
+ }
+ }
+
+ /**
+ * A {@link BufferedWriter} that does not buffer. It is generally more efficient to buffer at the
+ * {@link Content.Sink} level, since frequently total content is smaller than a single buffer and
+ * the {@link Content.Sink} can turn a close into a last write that will avoid chunking the
+ * response if at all possible. However, {@link BufferedWriter} is in the API for {@link
+ * HttpResponse}, so we must return a writer of that type.
+ */
+ private static class NonBufferedWriter extends BufferedWriter {
+ private final Writer writer;
+
+ public NonBufferedWriter(Writer out) {
+ super(out, 1);
+ writer = out;
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ writer.write(c);
+ }
+
+ @Override
+ public void write(char[] cbuf) throws IOException {
+ writer.write(cbuf);
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ writer.write(cbuf, off, len);
+ }
+
+ @Override
+ public void write(String str) throws IOException {
+ writer.write(str);
+ }
+
+ @Override
+ public void write(String str, int off, int len) throws IOException {
+ writer.write(str, off, len);
+ }
+
+ @Override
+ public Writer append(CharSequence csq) throws IOException {
+ return writer.append(csq);
+ }
+
+ @Override
+ public Writer append(CharSequence csq, int start, int end) throws IOException {
+ return writer.append(csq, start, end);
+ }
+
+ @Override
+ public Writer append(char c) throws IOException {
+ return writer.append(c);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ writer.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ writer.close();
+ }
+
+ @Override
+ public void newLine() throws IOException {
+ writer.write(System.lineSeparator());
}
}
}
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpUtil.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpUtil.java
new file mode 100644
index 00000000..042af255
--- /dev/null
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpUtil.java
@@ -0,0 +1,32 @@
+// Copyright 2025 Google LLC
+//
+// Licensed 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.
+
+package com.google.cloud.functions.invoker.http;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+
+class HttpUtil {
+ public static Map> toStringListMap(HttpFields headers) {
+ Map> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (HttpField field : headers) {
+ map.computeIfAbsent(field.getName(), key -> new ArrayList<>()).add(field.getValue());
+ }
+ return map;
+ }
+}
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/TimeoutFilter.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/TimeoutFilter.java
deleted file mode 100644
index e0577f9b..00000000
--- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/TimeoutFilter.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2024 Google LLC
-//
-// Licensed 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.
-
-package com.google.cloud.functions.invoker.http;
-
-import java.io.IOException;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.logging.Logger;
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletResponse;
-
-public class TimeoutFilter implements Filter {
-
- private static final Logger logger = Logger.getLogger(TimeoutFilter.class.getName());
- private final int timeoutMs;
-
- public TimeoutFilter(int timeoutSeconds) {
- this.timeoutMs = timeoutSeconds * 1000; // Convert seconds to milliseconds
- }
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- Timer timer = new Timer(true);
- TimerTask timeoutTask =
- new TimerTask() {
- @Override
- public void run() {
- if (response instanceof HttpServletResponse) {
- try {
- ((HttpServletResponse) response)
- .sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, "Request timed out");
- } catch (IOException e) {
- logger.warning("Error while sending HTTP response: " + e.toString());
- }
- } else {
- try {
- response.getWriter().write("Request timed out");
- } catch (IOException e) {
- logger.warning("Error while writing response: " + e.toString());
- }
- }
- }
- };
-
- timer.schedule(timeoutTask, timeoutMs);
-
- try {
- chain.doFilter(request, response);
- timeoutTask.cancel();
- } finally {
- timer.purge();
- }
- }
-}
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/TimeoutHandler.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/TimeoutHandler.java
new file mode 100644
index 00000000..8e0b1832
--- /dev/null
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/TimeoutHandler.java
@@ -0,0 +1,78 @@
+// Copyright 2025 Google LLC
+//
+// Licensed 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.
+
+package com.google.cloud.functions.invoker.http;
+
+import java.time.Duration;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.thread.Scheduler;
+
+public class TimeoutHandler extends Handler.Wrapper {
+ private final Duration timeout;
+
+ public TimeoutHandler(int timeoutSeconds, Handler handler) {
+ setHandler(handler);
+ timeout = Duration.ofSeconds(timeoutSeconds);
+ }
+
+ @Override
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ // Wrap the callback to ensure it is only completed once between the
+ // handler and the timeout task.
+ Callback wrappedCallback = new ProtectedCallback(callback);
+ Scheduler.Task timeoutTask =
+ request
+ .getComponents()
+ .getScheduler()
+ .schedule(
+ () -> wrappedCallback.failed(new TimeoutException("Function execution timed out")),
+ timeout);
+
+ // Cancel the timeout if the request completes the callback first.
+ return super.handle(request, response, Callback.from(timeoutTask::cancel, wrappedCallback));
+ }
+
+ private static class ProtectedCallback implements Callback {
+ private final Callback callback;
+ private final AtomicBoolean completed = new AtomicBoolean(false);
+
+ public ProtectedCallback(Callback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void succeeded() {
+ if (completed.compareAndSet(false, true)) {
+ callback.succeeded();
+ }
+ }
+
+ @Override
+ public void failed(Throwable x) {
+ if (completed.compareAndSet(false, true)) {
+ callback.failed(x);
+ }
+ }
+
+ @Override
+ public InvocationType getInvocationType() {
+ return callback.getInvocationType();
+ }
+ }
+}
\ No newline at end of file
diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java
index da5e72ec..20c3f248 100644
--- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java
+++ b/invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java
@@ -25,7 +25,7 @@
import com.google.cloud.functions.invoker.HttpFunctionExecutor;
import com.google.cloud.functions.invoker.TypedFunctionExecutor;
import com.google.cloud.functions.invoker.gcf.JsonLogHandler;
-import com.google.cloud.functions.invoker.http.TimeoutFilter;
+import com.google.cloud.functions.invoker.http.TimeoutHandler;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -39,32 +39,25 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
-import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
-import javax.servlet.DispatcherType;
-import javax.servlet.MultipartConfigElement;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
-import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.http.MultiPartConfig;
+import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.server.handler.HandlerWrapper;
-import org.eclipse.jetty.servlet.FilterHolder;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.server.handler.EagerContentHandler;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
/**
@@ -91,7 +84,7 @@ public class Invoker {
// if we arrange for them to be formatted using StackDriver's "structured
// logging" JSON format. Remove the JDK's standard logger and replace it with
// the JSON one.
- for (Handler handler : rootLogger.getHandlers()) {
+ for (java.util.logging.Handler handler : rootLogger.getHandlers()) {
rootLogger.removeHandler(handler);
}
rootLogger.addHandler(new JsonLogHandler(System.out, false));
@@ -242,7 +235,7 @@ ClassLoader getFunctionClassLoader() {
* unit or integration test, use {@link #startTestServer()} instead.
*
* @see #stopServer()
- * @throws Exception
+ * @throws Exception If there was a problem starting the server
*/
public void startServer() throws Exception {
startServer(true);
@@ -274,7 +267,7 @@ public void startServer() throws Exception {
* }
*
* @see #stopServer()
- * @throws Exception
+ * @throws Exception If there was a problem starting the server
*/
public void startTestServer() throws Exception {
startServer(false);
@@ -287,34 +280,46 @@ private void startServer(boolean join) throws Exception {
QueuedThreadPool pool = new QueuedThreadPool(1024);
server = new Server(pool);
+ server.setErrorHandler(
+ new ErrorHandler() {
+ @Override
+ protected void generateResponse(
+ Request request,
+ Response response,
+ int code,
+ String message,
+ Throwable cause,
+ Callback callback) {
+ // Suppress error body
+ callback.succeeded();
+ }
+ });
ServerConnector connector = new ServerConnector(server);
connector.setPort(port);
- server.setConnectors(new Connector[] {connector});
-
- ServletContextHandler servletContextHandler = new ServletContextHandler();
- servletContextHandler.setContextPath("/");
- server.setHandler(NotFoundHandler.forServlet(servletContextHandler));
+ connector.setReuseAddress(true);
+ connector.setReusePort(true);
+ server.addConnector(connector);
Class> functionClass = loadFunctionClass();
- HttpServlet servlet;
+ Handler handler;
if (functionSignatureType == null) {
- servlet = servletForDeducedSignatureType(functionClass);
+ handler = handlerForDeducedSignatureType(functionClass);
} else {
switch (functionSignatureType) {
case "http":
if (TypedFunction.class.isAssignableFrom(functionClass)) {
- servlet = TypedFunctionExecutor.forClass(functionClass);
+ handler = TypedFunctionExecutor.forClass(functionClass);
} else {
- servlet = HttpFunctionExecutor.forClass(functionClass);
+ handler = HttpFunctionExecutor.forClass(functionClass);
}
break;
case "event":
case "cloudevent":
- servlet = BackgroundFunctionExecutor.forClass(functionClass);
+ handler = BackgroundFunctionExecutor.forClass(functionClass);
break;
case "typed":
- servlet = TypedFunctionExecutor.forClass(functionClass);
+ handler = TypedFunctionExecutor.forClass(functionClass);
break;
default:
String error =
@@ -325,10 +330,18 @@ private void startServer(boolean join) throws Exception {
throw new RuntimeException(error);
}
}
- ServletHolder servletHolder = new ServletHolder(servlet);
- servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement(""));
- servletContextHandler.addServlet(servletHolder, "/*");
- servletContextHandler = addTimerFilterForRequestTimeout(servletContextHandler);
+
+ // Possibly wrap with TimeoutHandler if CLOUD_RUN_TIMEOUT_SECONDS is set.
+ handler = addTimerHandlerForRequestTimeout(handler);
+ server.setHandler(handler);
+
+ // Add a handler to asynchronously parse multipart before invoking the function.
+ MultiPartConfig config = new MultiPartConfig.Builder().maxMemoryPartSize(-1).build();
+ EagerContentHandler.MultiPartContentLoaderFactory factory =
+ new EagerContentHandler.MultiPartContentLoaderFactory(config);
+ server.insertHandler(new EagerContentHandler(factory));
+
+ server.insertHandler(new NotFoundHandler());
server.start();
logServerInfo();
@@ -376,7 +389,7 @@ private Class> loadFunctionClass() throws ClassNotFoundException {
}
}
- private HttpServlet servletForDeducedSignatureType(Class> functionClass) {
+ private Handler handlerForDeducedSignatureType(Class> functionClass) {
if (HttpFunction.class.isAssignableFrom(functionClass)) {
return HttpFunctionExecutor.forClass(functionClass);
}
@@ -398,16 +411,13 @@ private HttpServlet servletForDeducedSignatureType(Class> functionClass) {
throw new RuntimeException(error);
}
- private ServletContextHandler addTimerFilterForRequestTimeout(
- ServletContextHandler servletContextHandler) {
+ private Handler addTimerHandlerForRequestTimeout(Handler handler) {
String timeoutSeconds = System.getenv("CLOUD_RUN_TIMEOUT_SECONDS");
if (timeoutSeconds == null) {
- return servletContextHandler;
+ return handler;
}
int seconds = Integer.parseInt(timeoutSeconds);
- FilterHolder holder = new FilterHolder(new TimeoutFilter(seconds));
- servletContextHandler.addFilter(holder, "/*", EnumSet.of(DispatcherType.REQUEST));
- return servletContextHandler;
+ return new TimeoutHandler(seconds, handler);
}
static URL[] classpathToUrls(String classpath) {
@@ -468,32 +478,24 @@ private static boolean isGcf() {
/**
* Wrapper that intercepts requests for {@code /favicon.ico} and {@code /robots.txt} and causes
- * them to produce a 404 status. Otherwise they would be sent to the function code, like any other
- * URL, meaning that someone testing their function by using a browser as an HTTP client can see
- * two requests, one for {@code /favicon.ico} and one for {@code /} (or whatever).
+ * them to produce a 404 status. Otherwise, they would be sent to the function code, like any
+ * other URL, meaning that someone testing their function by using a browser as an HTTP client can
+ * see two requests, one for {@code /favicon.ico} and one for {@code /} (or whatever).
*/
- private static class NotFoundHandler extends HandlerWrapper {
- static NotFoundHandler forServlet(ServletContextHandler servletHandler) {
- NotFoundHandler handler = new NotFoundHandler();
- handler.setHandler(servletHandler);
- return handler;
- }
+ private static class NotFoundHandler extends Handler.Wrapper {
private static final Set NOT_FOUND_PATHS =
new HashSet<>(Arrays.asList("/favicon.ico", "/robots.txt"));
@Override
- public void handle(
- String target,
- Request baseRequest,
- HttpServletRequest request,
- HttpServletResponse response)
- throws IOException, ServletException {
- if (NOT_FOUND_PATHS.contains(request.getRequestURI())) {
- response.sendError(HttpStatus.NOT_FOUND_404, "Not Found");
- return;
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ if (NOT_FOUND_PATHS.contains(request.getHttpURI().getCanonicalPath())) {
+ response.setStatus(HttpStatus.NOT_FOUND_404);
+ callback.succeeded();
+ return true;
}
- super.handle(target, baseRequest, request, response);
+
+ return super.handle(request, response, callback);
}
}
@@ -522,7 +524,6 @@ private static class OnlyApiClassLoader extends ClassLoader {
protected Class> findClass(String name) throws ClassNotFoundException {
String prefix = "com.google.cloud.functions.";
if ((name.startsWith(prefix) && Character.isUpperCase(name.charAt(prefix.length())))
- || name.startsWith("javax.servlet.")
|| isCloudEventsApiClass(name)) {
return runtimeClassLoader.loadClass(name);
}
diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java
index 2f1d8bc8..c5ac7700 100644
--- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java
+++ b/invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java
@@ -47,6 +47,7 @@
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
+import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -66,16 +67,17 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
+import org.eclipse.jetty.client.ByteBufferRequestContent;
+import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.ContentProvider;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.util.BytesContentProvider;
-import org.eclipse.jetty.client.util.MultiPartContentProvider;
-import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.client.MultiPartRequestContent;
+import org.eclipse.jetty.client.Request;
+import org.eclipse.jetty.client.StringRequestContent;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MultiPart;
+import org.eclipse.jetty.http.MultiPart.ContentSourcePart;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
@@ -91,7 +93,7 @@ public class IntegrationTest {
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule public final TestName testName = new TestName();
- private static final String SERVER_READY_STRING = "Started ServerConnector";
+ private static final String SERVER_READY_STRING = "Started oejs.ServerConnector";
private static final String EXECUTION_ID_HTTP_HEADER = "HTTP_FUNCTION_EXECUTION_ID";
private static final String EXECUTION_ID = "1234abcd";
@@ -167,10 +169,19 @@ abstract static class TestCase {
abstract String url();
- abstract ContentProvider requestContent();
+ abstract Request.Content requestContent();
abstract int expectedResponseCode();
+ /**
+ * Expected response headers map, header name -> value.
+ * Value "*" asserts the header is present with any value.
+ * Value "-" asserts the header is not present.
+ *
+ * @return the expected response headers for this test case.
+ */
+ abstract Optional