From 1b37e5f87927fb65a47c691047d610216acaaad8 Mon Sep 17 00:00:00 2001 From: "@James" Date: Mon, 1 Dec 2025 21:48:20 +0800 Subject: [PATCH] Bump version to 1.0.4 and improve server lifecycle management Version Updates: - Bump module version from 1.0.3 to 1.0.4 - Upgrade tinystruct dependency from 1.7.10 to 1.7.12 - Upgrade central-publishing-maven-plugin from 0.7.0 to 0.8.0 Server Lifecycle Improvements: - Add proper shutdown hook registration before server start - Store ChannelFuture as instance variable for proper cleanup - Improve stop() method with channel close synchronization - Remove redundant shutdown hook and finally block - Disable template requirement in init() SSL Configuration Enhancement: - Extract SSL configuration to dedicated configureSsl() method - Add support for custom SSL certificates via configuration - Read certificate/key paths from settings (ssl.certificate.path, ssl.key.path) - Fall back to self-signed certificate with warning if paths not provided - Add logging for production SSL configuration recommendations Bug Fixes: - Fix SSE Content-Type header from invalid "text/event-stream, application/json" to proper "text/event-stream; charset=utf-8" - Simplify exceptionCaught() method signature by removing Context parameter Code Quality: - Add missing File import - Improve error handling in channel close with InterruptedException handling - Better separation of concerns with SSL configuration extraction --- pom.xml | 6 +- .../handler/HttpRequestHandler.java | 2 +- .../tinystruct/system/NettyHttpServer.java | 65 ++++++++++++++----- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index bad4e02..1439f1c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ netty http server module org.tinystruct tinystruct-netty-http-server - 1.0.3 + 1.0.4 A tinystruct-based module to enable netty http server support. https://tinystruct.org @@ -32,7 +32,7 @@ 17 UTF-8 4.2.7.Final - 1.7.10 + 1.7.12 6.0.1 @@ -233,7 +233,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.7.0 + 0.8.0 true central diff --git a/src/main/java/org/tinystruct/handler/HttpRequestHandler.java b/src/main/java/org/tinystruct/handler/HttpRequestHandler.java index 885b258..1e0e0ae 100644 --- a/src/main/java/org/tinystruct/handler/HttpRequestHandler.java +++ b/src/main/java/org/tinystruct/handler/HttpRequestHandler.java @@ -247,7 +247,7 @@ private SSEPushManager getAppropriatePushManager(boolean isMCP) { private void handleSSE(final ChannelHandlerContext ctx, final Request request, Response response, final Context context, boolean keepAlive) { // Set SSE headers using the existing response infrastructure - response.addHeader(Header.CONTENT_TYPE.name(), "text/event-stream, application/json"); + response.addHeader(Header.CONTENT_TYPE.name(), "text/event-stream; charset=utf-8"); response.addHeader(Header.CACHE_CONTROL.name(), "no-cache"); response.addHeader(Header.CONNECTION.name(), "keep-alive"); response.addHeader(Header.TRANSFER_ENCODING.name(), "chunked"); diff --git a/src/main/java/org/tinystruct/system/NettyHttpServer.java b/src/main/java/org/tinystruct/system/NettyHttpServer.java index b231c32..b07f84a 100644 --- a/src/main/java/org/tinystruct/system/NettyHttpServer.java +++ b/src/main/java/org/tinystruct/system/NettyHttpServer.java @@ -42,6 +42,7 @@ import org.tinystruct.system.annotation.Action; import org.tinystruct.system.annotation.Argument; +import java.io.File; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; @@ -71,6 +72,7 @@ public class NettyHttpServer extends AbstractApplication implements Bootstrap { private final EventLoopGroup workgroup; private final Logger logger = Logger.getLogger(NettyHttpServer.class.getName()); private int port = 8080; + private ChannelFuture future; public NettyHttpServer() { if (Epoll.isAvailable()) { @@ -84,7 +86,7 @@ public NettyHttpServer() { @Override public void init() { - + this.setTemplateRequired(false); } @Override @@ -129,6 +131,12 @@ public void start() throws ApplicationException { } } + // Add shutdown hook BEFORE starting server + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + logger.info("Shutting down HTTP server..."); + stop(); + })); + System.out.println(ApplicationManager.call("--logo", null, Action.Mode.CLI)); String charsetName = null; @@ -156,8 +164,7 @@ public void start() throws ApplicationException { // Configure SSL. final SslContext sslCtx; if (SSL) { - SelfSignedCertificate ssc = new SelfSignedCertificate(); - sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); + sslCtx = configureSsl(settings); } else { sslCtx = null; } @@ -190,7 +197,7 @@ public void initChannel(SocketChannel ch) { }).option(ChannelOption.SO_BACKLOG, 1024).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000).childOption(ChannelOption.SO_KEEPALIVE, false).childOption(ChannelOption.TCP_NODELAY, true); // Bind and start to accept incoming connections. - ChannelFuture future = bootstrap.bind(port).sync(); + future = bootstrap.bind(port).sync(); logger.info("Netty server (" + port + ") startup in " + (System.currentTimeMillis() - start) + " ms"); // Open the default browser @@ -200,32 +207,58 @@ public void initChannel(SocketChannel ch) { // Keep the server running logger.info("Server is running. Press Ctrl+C to stop."); - // Add shutdown hook - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - logger.info("Shutting down HTTP server..."); - stop(); - })); - // Wait until the server socket is closed. future.channel().closeFuture().sync(); } catch (Exception e) { throw new ApplicationException(e.getMessage(), e.getCause()); - } finally { - this.stop(); } } @Override public void stop() { + // Close the channel first + if (future != null && future.channel().isOpen()) { + try { + future.channel().close().sync(); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Interrupted while closing channel", e); + Thread.currentThread().interrupt(); + } + } + + // Then shutdown event loops gracefully bossgroup.shutdownGracefully(); workgroup.shutdownGracefully(); } - @Action(value = "error", description = "Error page") - public Object exceptionCaught() throws ApplicationException { - Request request = (Request) getContext().getAttribute(HTTP_REQUEST); - Response response = (Response) getContext().getAttribute(HTTP_RESPONSE); + private SslContext configureSsl(Configuration settings) throws Exception { + if (!SSL) { + return null; + } + + String certPath = settings.get("ssl.certificate.path"); + String keyPath = settings.get("ssl.key.path"); + if (!certPath.isEmpty() && !keyPath.isEmpty()) { + // Use provided certificate + return SslContextBuilder.forServer( + new File(certPath), + new File(keyPath) + ).build(); + } else { + // Fall back to self-signed certificate + logger.warning("Using self-signed certificate. " + + "Configure ssl.certificate.path and ssl.key.path for production."); + SelfSignedCertificate ssc = new SelfSignedCertificate(); + return SslContextBuilder.forServer( + ssc.certificate(), + ssc.privateKey() + ).build(); + } + } + + @Action(value = "error", description = "Error page") + public Object exceptionCaught(Request request, Response response) throws ApplicationException { Reforward reforward = new Reforward(request, response); this.setVariable("from", reforward.getFromURL());