diff --git a/avaje-jex-grizzly-spi/pom.xml b/avaje-jex-grizzly-spi/pom.xml
new file mode 100644
index 00000000..e7c61d20
--- /dev/null
+++ b/avaje-jex-grizzly-spi/pom.xml
@@ -0,0 +1,39 @@
+
+ 4.0.0
+
+ io.avaje
+ avaje-jex-parent
+ 3.0-RC23
+
+ 0.1
+ avaje-jex-grizzly-spi
+
+
+
+
+ io.avaje
+ avaje-jex
+
+
+
+ org.glassfish.grizzly
+ grizzly-http-server
+ 4.1.0-M1
+
+
+ io.avaje
+ avaje-spi-service
+ provided
+ true
+
+
+
+ io.avaje
+ avaje-jex-test
+ test
+
+
+
+
diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyExchange.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyExchange.java
new file mode 100644
index 00000000..10c7fb4f
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyExchange.java
@@ -0,0 +1,10 @@
+package io.avaje.jex.grizzly.spi;
+
+import com.sun.net.httpserver.HttpPrincipal;
+
+sealed interface GrizzlyExchange permits GrizzlyHttpExchange, GrizzlyHttpsExchange {
+
+ HttpPrincipal getPrincipal();
+
+ void setPrincipal(HttpPrincipal principal);
+}
diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHandler.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHandler.java
new file mode 100644
index 00000000..4ea79c85
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHandler.java
@@ -0,0 +1,48 @@
+package io.avaje.jex.grizzly.spi;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import org.glassfish.grizzly.http.server.Request;
+import org.glassfish.grizzly.http.server.Response;
+
+import com.sun.net.httpserver.Filter.Chain;
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+final class GrizzlyHandler extends org.glassfish.grizzly.http.server.HttpHandler {
+
+ private final HttpContext httpContext;
+
+ private HttpHandler handler;
+
+ GrizzlyHandler(HttpContext httpContext, HttpHandler httpHandler) {
+ super(httpContext.getPath());
+ this.httpContext = httpContext;
+ this.handler = httpHandler;
+ }
+
+ @Override
+ public void service(Request request, Response response) {
+
+ try (HttpExchange exchange =
+ request.isSecure()
+ ? new GrizzlyHttpsExchange(httpContext, request, response)
+ : new GrizzlyHttpExchange(httpContext, request, response)) {
+
+ new Chain(httpContext.getFilters(), handler).doFilter(exchange);
+
+ } catch (IOException ex) {
+ throw new UncheckedIOException(null);
+ }
+ }
+
+ public HttpHandler getHttpHandler() {
+ return handler;
+ }
+
+ public void setHttpHandler(HttpHandler handler) {
+ this.handler = handler;
+ }
+}
diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpContext.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpContext.java
new file mode 100644
index 00000000..5ede6a56
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpContext.java
@@ -0,0 +1,77 @@
+package io.avaje.jex.grizzly.spi;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.sun.net.httpserver.Authenticator;
+import com.sun.net.httpserver.Filter;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+final class GrizzlyHttpContext extends com.sun.net.httpserver.HttpContext {
+
+ private final GrizzlyHandler grizzlyHandler;
+ private final HttpServer server;
+
+ private final Map attributes = new HashMap<>();
+
+ private final List filters = new ArrayList<>();
+
+ private Authenticator authenticator;
+
+ private String contextPath;
+
+ protected GrizzlyHttpContext(HttpServer server, String contextPath, HttpHandler handler) {
+ this.server = server;
+ this.grizzlyHandler = new GrizzlyHandler(this, handler);
+ this.contextPath = contextPath;
+ }
+
+ GrizzlyHandler getGrizzlyHandler() {
+ return grizzlyHandler;
+ }
+
+ @Override
+ public HttpHandler getHandler() {
+ return grizzlyHandler.getHttpHandler();
+ }
+
+ @Override
+ public void setHandler(HttpHandler h) {
+ grizzlyHandler.setHttpHandler(h);
+ }
+
+ @Override
+ public String getPath() {
+ return contextPath;
+ }
+
+ @Override
+ public HttpServer getServer() {
+ return server;
+ }
+
+ @Override
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public List getFilters() {
+ return filters;
+ }
+
+ @Override
+ public Authenticator setAuthenticator(Authenticator auth) {
+ Authenticator previous = authenticator;
+ authenticator = auth;
+ return previous;
+ }
+
+ @Override
+ public Authenticator getAuthenticator() {
+ return authenticator;
+ }
+}
diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchange.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchange.java
new file mode 100644
index 00000000..38fc59c8
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchange.java
@@ -0,0 +1,129 @@
+package io.avaje.jex.grizzly.spi;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.URI;
+
+import org.glassfish.grizzly.http.server.Request;
+import org.glassfish.grizzly.http.server.Response;
+
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpPrincipal;
+
+final class GrizzlyHttpExchange extends HttpExchange implements GrizzlyExchange {
+ private final GrizzlyHttpExchangeDelegate delegate;
+
+ public GrizzlyHttpExchange(HttpContext context, Request req, Response resp) {
+
+ delegate = new GrizzlyHttpExchangeDelegate(context, req, resp);
+ }
+
+ @Override
+ public int hashCode() {
+ return delegate.hashCode();
+ }
+
+ @Override
+ public Headers getRequestHeaders() {
+ return delegate.getRequestHeaders();
+ }
+
+ @Override
+ public Headers getResponseHeaders() {
+ return delegate.getResponseHeaders();
+ }
+
+ @Override
+ public URI getRequestURI() {
+ return delegate.getRequestURI();
+ }
+
+ @Override
+ public String getRequestMethod() {
+ return delegate.getRequestMethod();
+ }
+
+ @Override
+ public HttpContext getHttpContext() {
+ return delegate.getHttpContext();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return delegate.equals(obj);
+ }
+
+ @Override
+ public InputStream getRequestBody() {
+ return delegate.getRequestBody();
+ }
+
+ @Override
+ public OutputStream getResponseBody() {
+ return delegate.getResponseBody();
+ }
+
+ @Override
+ public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
+ delegate.sendResponseHeaders(rCode, responseLength);
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress() {
+ return delegate.getRemoteAddress();
+ }
+
+ @Override
+ public int getResponseCode() {
+ return delegate.getResponseCode();
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress() {
+ return delegate.getLocalAddress();
+ }
+
+ @Override
+ public String getProtocol() {
+ return delegate.getProtocol();
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return delegate.getAttribute(name);
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ delegate.setAttribute(name, value);
+ }
+
+ @Override
+ public void setStreams(InputStream i, OutputStream o) {
+ delegate.setStreams(i, o);
+ }
+
+ @Override
+ public HttpPrincipal getPrincipal() {
+ return delegate.getPrincipal();
+ }
+
+ @Override
+ public void setPrincipal(HttpPrincipal principal) {
+ delegate.setPrincipal(principal);
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+}
diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchangeDelegate.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchangeDelegate.java
new file mode 100644
index 00000000..a6b6982c
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchangeDelegate.java
@@ -0,0 +1,193 @@
+package io.avaje.jex.grizzly.spi;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.glassfish.grizzly.http.server.Request;
+import org.glassfish.grizzly.http.server.Response;
+
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpPrincipal;
+
+final class GrizzlyHttpExchangeDelegate extends HttpExchange {
+
+ /** Set of headers that RFC9110 says will not have a value list */
+ private static final Set SINGLE_VALUE_HEADERS =
+ Set.of(
+ "authorization",
+ "content-length",
+ "date",
+ "expires",
+ "host",
+ "if-modified-since",
+ "if-unmodified-since",
+ "if-range",
+ "last-modified",
+ "location",
+ "referer",
+ "retry-after",
+ "user-agent");
+
+ private final HttpContext context;
+
+ private final Request request;
+
+ private final Headers responseHeaders = new Headers();
+
+ private Headers requestHeaders = new Headers();
+
+ private int statusCode = 0;
+
+ private InputStream inputStream;
+
+ private OutputStream outputStream;
+
+ private HttpPrincipal httpPrincipal;
+
+ private Response response;
+
+ GrizzlyHttpExchangeDelegate(HttpContext httpSpiContext, Request request, Response response) {
+ this.context = httpSpiContext;
+ this.request = request;
+ this.response = response;
+ this.inputStream = request.getInputStream();
+ this.outputStream = response.getOutputStream();
+ }
+
+ @Override
+ public Headers getRequestHeaders() {
+
+ if (!requestHeaders.isEmpty()) {
+ return requestHeaders;
+ }
+ for (var name : request.getHeaderNames()) {
+
+ if (!SINGLE_VALUE_HEADERS.contains(name.toLowerCase())) {
+
+ for (String value : request.getHeaders(name)) {
+ requestHeaders.add(name, value);
+ }
+ } else {
+ requestHeaders.add(name, request.getHeader(name));
+ }
+ }
+ return requestHeaders;
+ }
+
+ @Override
+ public Headers getResponseHeaders() {
+ return responseHeaders;
+ }
+
+ @Override
+ public URI getRequestURI() {
+ return URI.create(request.getRequestURI());
+ }
+
+ @Override
+ public String getRequestMethod() {
+ return request.getMethod().getMethodString();
+ }
+
+ @Override
+ public HttpContext getHttpContext() {
+ return context;
+ }
+
+ @Override
+ public void close() {
+ try {
+ outputStream.close();
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ @Override
+ public InputStream getRequestBody() {
+ return inputStream;
+ }
+
+ @Override
+ public OutputStream getResponseBody() {
+ return outputStream;
+ }
+
+ @Override
+ public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
+ this.statusCode = rCode;
+
+ for (Map.Entry> stringListEntry : responseHeaders.entrySet()) {
+ String name = stringListEntry.getKey();
+ List values = stringListEntry.getValue();
+ for (String value : values) {
+ response.addHeader(name, value);
+ }
+ }
+
+ if (responseLength == -1) {
+ response.setContentLengthLong(0);
+ } else if (responseLength == 0) {
+ response.setContentLengthLong(-1);
+ } else {
+ response.setContentLengthLong(responseLength);
+ }
+
+ response.setStatus(rCode);
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress() {
+ return InetSocketAddress.createUnresolved(request.getRemoteAddr(), request.getRemotePort());
+ }
+
+ @Override
+ public int getResponseCode() {
+ return statusCode;
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress() {
+ return new InetSocketAddress(request.getLocalAddr(), request.getLocalPort());
+ }
+
+ @Override
+ public String getProtocol() {
+ return request.getProtocol().getProtocolString();
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return request.getAttribute(name);
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ request.setAttribute(name, value);
+ }
+
+ @Override
+ public void setStreams(InputStream i, OutputStream o) {
+ assert inputStream != null;
+ if (i != null) inputStream = i;
+ if (o != null) outputStream = o;
+ }
+
+ @Override
+ public HttpPrincipal getPrincipal() {
+ return httpPrincipal;
+ }
+
+ public void setPrincipal(HttpPrincipal principal) {
+ this.httpPrincipal = principal;
+ }
+}
diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServer.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServer.java
new file mode 100644
index 00000000..cdd9f79c
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServer.java
@@ -0,0 +1,161 @@
+package io.avaje.jex.grizzly.spi;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.System.Logger.Level;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.glassfish.grizzly.http.server.HttpServer;
+import org.glassfish.grizzly.http.server.NetworkListener;
+import org.glassfish.grizzly.http.server.ServerConfiguration;
+import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
+
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpsConfigurator;
+
+final class GrizzlyHttpServer extends com.sun.net.httpserver.HttpsServer {
+ private static final System.Logger LOG =
+ System.getLogger(GrizzlyHttpServer.class.getCanonicalName());
+ private final HttpServer server;
+ private InetSocketAddress addr;
+ private ServerConfiguration httpConfiguration;
+ private ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
+ private HttpsConfigurator httpsConfig;
+
+ public GrizzlyHttpServer(HttpServer server) {
+
+ this(server, server.getServerConfiguration());
+ }
+
+ public GrizzlyHttpServer(HttpServer server, ServerConfiguration configuration) {
+ this.server = server;
+ this.httpConfiguration = configuration;
+ }
+
+ public ServerConfiguration getHttpConfiguration() {
+ return httpConfiguration;
+ }
+
+ @Override
+ public void bind(InetSocketAddress addr, int backlog) throws IOException {
+
+ this.addr = addr;
+ // check if there is already a connector listening
+ var connectors = server.getListeners();
+ if (connectors != null) {
+ for (var connector : connectors) {
+ if (connector.getPort() == addr.getPort()) {
+ LOG.log(
+ Level.DEBUG, "server already bound to port {}, no need to rebind", addr.getPort());
+ return;
+ }
+ }
+ }
+
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.log(Level.DEBUG, "binding server to port " + addr.getPort());
+ }
+ var listener = new NetworkListener("rizzly", addr.getHostName(), addr.getPort());
+ listener.getTransport().setWorkerThreadPool(executor);
+ if (backlog != 0) {
+ listener.getTransport().setServerConnectionBackLog(backlog);
+ }
+ if (httpsConfig != null) {
+ listener.setSSLEngineConfig(new SSLEngineConfigurator(httpsConfig.getSSLContext()));
+ }
+
+ server.addListener(listener);
+ }
+
+ protected HttpServer getServer() {
+ return server;
+ }
+
+ @Override
+ public InetSocketAddress getAddress() {
+ if (addr.getPort() == 0 && server.isStarted()) {
+ return new InetSocketAddress(addr.getHostString(), server.getListener("rizzly").getPort());
+ }
+ return addr;
+ }
+
+ @Override
+ public void start() {
+
+ try {
+ server.start();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public void setExecutor(Executor executor) {
+ if (executor instanceof ExecutorService service) {
+ this.executor = service;
+ } else {
+ throw new IllegalArgumentException("Grizzly only accepts an instance of ExecutorService");
+ }
+ }
+
+ @Override
+ public Executor getExecutor() {
+ return executor;
+ }
+
+ @Override
+ public void stop(int delay) {
+ server.shutdownNow();
+ }
+
+ @Override
+ public HttpContext createContext(String path, HttpHandler httpHandler) {
+
+ GrizzlyHttpContext context = new GrizzlyHttpContext(this, path, httpHandler);
+ GrizzlyHandler jettyContextHandler = context.getGrizzlyHandler();
+
+ httpConfiguration.addHttpHandler(
+ jettyContextHandler, path.transform(this::prependSlash).transform(this::appendSlash));
+
+ return context;
+ }
+
+ private String prependSlash(String s) {
+ return s.startsWith("/") ? s : "/" + s;
+ }
+
+ private String appendSlash(String s) {
+ return s.endsWith("/") ? s + "*" : s + "/*";
+ }
+
+ @Override
+ public HttpContext createContext(String path) {
+ return createContext(path, null);
+ }
+
+ @Override
+ public void removeContext(String path) {
+
+ throw new UnsupportedOperationException("notImplemented");
+ }
+
+ @Override
+ public void removeContext(HttpContext context) {
+
+ throw new UnsupportedOperationException("notImplemented");
+ }
+
+ @Override
+ public void setHttpsConfigurator(HttpsConfigurator config) {
+ httpsConfig = config;
+ }
+
+ @Override
+ public HttpsConfigurator getHttpsConfigurator() {
+ return httpsConfig;
+ }
+}
diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServerProvider.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServerProvider.java
new file mode 100644
index 00000000..bd21367d
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServerProvider.java
@@ -0,0 +1,50 @@
+package io.avaje.jex.grizzly.spi;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import org.glassfish.grizzly.http.server.HttpServer;
+
+import com.sun.net.httpserver.HttpsServer;
+import com.sun.net.httpserver.spi.HttpServerProvider;
+
+import io.avaje.spi.ServiceProvider;
+
+@ServiceProvider
+public class GrizzlyHttpServerProvider extends HttpServerProvider {
+
+ private org.glassfish.grizzly.http.server.HttpServer server;
+
+ public GrizzlyHttpServerProvider(HttpServer server) {
+
+ this.server = server;
+ }
+
+ public GrizzlyHttpServerProvider() {
+
+ this.server = new HttpServer();
+ }
+
+ @Override
+ public com.sun.net.httpserver.HttpServer createHttpServer(InetSocketAddress addr, int backlog)
+ throws IOException {
+
+ return createServer(addr, backlog);
+ }
+
+ @Override
+ public HttpsServer createHttpsServer(InetSocketAddress addr, int backlog) throws IOException {
+ return createServer(addr, backlog);
+ }
+
+ private com.sun.net.httpserver.HttpsServer createServer(InetSocketAddress addr, int backlog)
+ throws IOException {
+ if (server == null) {
+ server = new HttpServer();
+ }
+
+ GrizzlyHttpServer jettyHttpServer = new GrizzlyHttpServer(server);
+ if (addr != null) jettyHttpServer.bind(addr, backlog);
+ return jettyHttpServer;
+ }
+}
diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpsExchange.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpsExchange.java
new file mode 100644
index 00000000..d1e686d6
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpsExchange.java
@@ -0,0 +1,125 @@
+package io.avaje.jex.grizzly.spi;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.URI;
+
+import javax.net.ssl.SSLSession;
+
+import org.glassfish.grizzly.http.server.Request;
+import org.glassfish.grizzly.http.server.Response;
+
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpPrincipal;
+import com.sun.net.httpserver.HttpsExchange;
+
+final class GrizzlyHttpsExchange extends HttpsExchange implements GrizzlyExchange {
+ private final GrizzlyHttpExchangeDelegate delegate;
+
+ public GrizzlyHttpsExchange(HttpContext jaxWsContext, Request req, Response resp) {
+ delegate = new GrizzlyHttpExchangeDelegate(jaxWsContext, req, resp);
+ }
+
+ @Override
+ public Headers getRequestHeaders() {
+ return delegate.getRequestHeaders();
+ }
+
+ @Override
+ public Headers getResponseHeaders() {
+ return delegate.getResponseHeaders();
+ }
+
+ @Override
+ public URI getRequestURI() {
+ return delegate.getRequestURI();
+ }
+
+ @Override
+ public String getRequestMethod() {
+ return delegate.getRequestMethod();
+ }
+
+ @Override
+ public HttpContext getHttpContext() {
+ return delegate.getHttpContext();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public InputStream getRequestBody() {
+ return delegate.getRequestBody();
+ }
+
+ @Override
+ public OutputStream getResponseBody() {
+ return delegate.getResponseBody();
+ }
+
+ @Override
+ public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
+ delegate.sendResponseHeaders(rCode, responseLength);
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress() {
+ return delegate.getRemoteAddress();
+ }
+
+ @Override
+ public int getResponseCode() {
+ return delegate.getResponseCode();
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress() {
+ return delegate.getLocalAddress();
+ }
+
+ @Override
+ public String getProtocol() {
+ return delegate.getProtocol();
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return delegate.getAttribute(name);
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ delegate.setAttribute(name, value);
+ }
+
+ @Override
+ public void setStreams(InputStream i, OutputStream o) {
+ delegate.setStreams(i, o);
+ }
+
+ @Override
+ public HttpPrincipal getPrincipal() {
+ return delegate.getPrincipal();
+ }
+
+ @Override
+ public void setPrincipal(HttpPrincipal principal) {
+ delegate.setPrincipal(principal);
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ @Override
+ public SSLSession getSSLSession() {
+ return null;
+ }
+}
diff --git a/avaje-jex-grizzly-spi/src/main/java/module-info.java b/avaje-jex-grizzly-spi/src/main/java/module-info.java
new file mode 100644
index 00000000..6378b580
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/main/java/module-info.java
@@ -0,0 +1,17 @@
+import com.sun.net.httpserver.spi.HttpServerProvider;
+
+module io.avaje.jex.grizzly {
+
+ exports io.avaje.jex.grizzly.spi;
+
+ requires transitive io.avaje.jex;
+ requires transitive jdk.httpserver;
+ requires transitive org.glassfish.grizzly.http.server;
+ requires transitive org.glassfish.grizzly.http;
+ requires transitive org.glassfish.grizzly;
+
+ requires static io.avaje.spi;
+ requires static java.net.http;
+
+ provides HttpServerProvider with io.avaje.jex.grizzly.spi.GrizzlyHttpServerProvider;
+}
diff --git a/avaje-jex-grizzly-spi/src/test/java/io/avaje/jex/grizzly/spi/FilterTest.java b/avaje-jex-grizzly-spi/src/test/java/io/avaje/jex/grizzly/spi/FilterTest.java
new file mode 100644
index 00000000..fc6cc485
--- /dev/null
+++ b/avaje-jex-grizzly-spi/src/test/java/io/avaje/jex/grizzly/spi/FilterTest.java
@@ -0,0 +1,119 @@
+package io.avaje.jex.grizzly.spi;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.http.HttpHeaders;
+import java.net.http.HttpResponse;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.LockSupport;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+
+import io.avaje.jex.Jex;
+import io.avaje.jex.test.TestPair;
+
+class FilterTest {
+
+ static final TestPair pair = init();
+ static final AtomicReference afterAll = new AtomicReference<>();
+ static final AtomicReference afterTwo = new AtomicReference<>();
+
+ static TestPair init() {
+ final Jex app =
+ Jex.create()
+ .routing(
+ routing ->
+ routing
+ .get("/", ctx -> ctx.text("roo"))
+ .get(
+ "/noResponse",
+ ctx -> {
+ ctx.header("Content-Type", "");
+ })
+ .get("/one", ctx -> ctx.text("one"))
+ .get("/two", ctx -> ctx.text("two"))
+ .get("/two/{id}", ctx -> ctx.text("two-id"))
+ .before(ctx -> ctx.header("before-all", "set"))
+ .filter(
+ (ctx, chain) -> {
+ if (ctx.path().contains("/two/")) {
+ ctx.header("before-two", "set");
+ }
+ chain.proceed();
+ })
+ .after(ctx -> afterAll.set("set"))
+ .filter(
+ (ctx, chain) -> {
+ chain.proceed();
+ if (ctx.path().contains("/two/")) {
+ afterTwo.set("set");
+ }
+ })
+ .get("/dummy", ctx -> ctx.text("dummy")));
+
+ return TestPair.create(app);
+ }
+
+ @AfterAll
+ static void end() {
+ pair.shutdown();
+ }
+
+ void clearAfter() {
+ afterAll.set(null);
+ afterTwo.set(null);
+ }
+
+ @Test
+ void get() {
+ clearAfter();
+ HttpResponse res = pair.request().GET().asString();
+ assertHasBeforeAfterAll(res);
+ assertNoBeforeAfterTwo(res);
+
+ clearAfter();
+ res = pair.request().path("one").GET().asString();
+ assertHasBeforeAfterAll(res);
+ assertNoBeforeAfterTwo(res);
+
+ clearAfter();
+ res = pair.request().path("two").GET().asString();
+ assertHasBeforeAfterAll(res);
+ assertNoBeforeAfterTwo(res);
+ }
+
+ @Test
+ void getNoResponse() {
+ clearAfter();
+ HttpResponse res = pair.request().path("noResponse").GET().asString();
+ assertThat(res.statusCode()).isEqualTo(204);
+ assertHasBeforeAfterAll(res);
+ assertNoBeforeAfterTwo(res);
+ }
+
+ @Test
+ void get_two_expect_extraFilters() {
+ clearAfter();
+ HttpResponse res = pair.request().path("two/42").GET().asString();
+
+ final HttpHeaders headers = res.headers();
+ assertHasBeforeAfterAll(res);
+ assertThat(headers.firstValue("before-two")).get().isEqualTo("set");
+ assertThat(afterTwo.get()).isEqualTo("set");
+ }
+
+ private void assertNoBeforeAfterTwo(HttpResponse res) {
+ assertThat(res.statusCode()).isLessThan(300);
+ assertThat(res.headers().firstValue("before-two")).isEmpty();
+ assertThat(afterTwo.get()).isNull();
+ }
+
+ private void assertHasBeforeAfterAll(HttpResponse res) {
+ assertThat(res.statusCode()).isLessThan(300);
+ assertThat(res.headers().firstValue("before-all")).get().isEqualTo("set");
+ LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(2));
+ assertThat(afterAll.get()).isEqualTo("set");
+ }
+}
diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java b/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java
index cd91a6ef..f224049d 100644
--- a/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java
+++ b/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java
@@ -60,7 +60,7 @@ public void handle(HttpExchange exchange) {
}
private void handleNoResponse(HttpExchange exchange) throws IOException {
- if (exchange.getResponseCode() == -1) {
+ if (exchange.getResponseCode() < 1) {
exchange.sendResponseHeaders(204, -1);
}
}
diff --git a/pom.xml b/pom.xml
index 4a7f6556..3cb28519 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,11 +39,12 @@
avaje-jex
- avaje-jex-test
avaje-jex-freemarker
- avaje-jex-mustache
+ avaje-jex-grizzly-spi
avaje-jex-htmx
+ avaje-jex-mustache
avaje-jex-static-content
+ avaje-jex-test