SCX HTTP Routing X 是 scx-http-routing 的扩展处理器库。
它提供了一组常用的 HTTP Routing handler,用来补充路由核心本身没有内置的能力,例如 CORS、静态文件服务、单文件返回、Range 请求、HTTP 缓存验证、Cache-Control 构造等。
SCX HTTP Routing X 本身不是 HTTP Server,也不是路由核心。HTTP 请求、响应、路由匹配和 RoutingContext 来自 scx-http 与 scx-http-routing。本模块只是提供可以挂载到路由上的常用处理器。
当前版本为 0.3.0。
<dependency>
<groupId>dev.scx</groupId>
<artifactId>scx-http-routing-x</artifactId>
<version>0.3.0</version>
</dependency>
scx-http-routing-x 依赖:
scx-http-routing
因此在普通使用场景中,只需要引入 scx-http-routing-x,就可以同时获得 scx-http-routing 的路由核心能力。
SCX HTTP Routing X 主要包含三类 handler:
CorsHandler CORS 处理器
StaticFilesHandler 目录型静态文件处理器
SingleFileHandler 单文件处理器
同时包含几组辅助对象:
AllowOrigin CORS Origin 策略
AllowMethods CORS Method 策略
AllowHeaders CORS Header 策略
ExposeHeaders CORS Expose Header 策略
CacheControl Cache-Control 响应头构造器
Range Range 请求头模型
ContentRange Content-Range 响应头模型
HttpDateHelper HTTP-date 编解码工具
HttpValidator ETag / Last-Modified 验证器
StaticFilesSupport 静态文件响应工具
它们之间的关系可以简单理解为:
Router
↓
route(..., CorsHandler)
route(..., StaticFilesHandler)
route(..., SingleFileHandler)
↓
处理 CORS / 静态文件 / 单文件响应
静态文件相关流程可以理解为:
StaticFilesHandler / SingleFileHandler
↓
定位文件
↓
设置 Cache-Control
↓
StaticFilesSupport.serveFile(...)
↓
ETag / Last-Modified / If-None-Match / If-Modified-Since
↓
StaticFilesSupport.sendFile(...)
↓
Range / Content-Range / 206 / 416 / 完整文件响应
import dev.scx.http.routing.Router;
import dev.scx.http.routing.x.cors.CorsHandler;
import dev.scx.http.routing.x.cors.allow_origin.AllowOrigin;
var router = Router.of();
router.route(
-10000,
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
.allowCredentials(true)
);
这里使用较小的 route order,让 CORS 尽可能靠前执行。
import dev.scx.http.routing.Router;
import dev.scx.http.routing.x.static_files.StaticFilesHandler;
import java.nio.file.Path;
var router = Router.of();
router.route(
"/*",
StaticFilesHandler.of(Path.of("./public"))
);
如果请求:
/assets/app.js
并且路由模板是:
/*
那么 * 捕获会被转换成相对路径,然后从 ./public 目录下查找对应文件。
import dev.scx.http.routing.Router;
import dev.scx.http.routing.x.single_file.SingleFileHandler;
import java.nio.file.Path;
var router = Router.of();
router.route(
"/*",
SingleFileHandler.of(Path.of("./public/index.html"))
);
这常用于 SPA fallback。
所有匹配到该路由的 GET / HEAD 请求,都会尝试返回同一个文件。
import dev.scx.http.routing.x.static_files.StaticFilesSupport;
import java.io.File;
router.route("/download", ctx -> {
var file = new File("./files/demo.zip");
StaticFilesSupport.sendFile(file, ctx.request());
});
如果希望支持 ETag / Last-Modified / 304:
router.route("/image", ctx -> {
var file = new File("./files/image.png");
StaticFilesSupport.serveFile(file, ctx.request());
});
CorsHandler 是 CORS 处理器。
它实现了:
Function1Void<RoutingContext, Throwable>
因此可以直接挂载到 Router 上。
router.route(
-10000,
CorsHandler.of()
);
默认配置是:
allowOrigin reflect
allowMethods reflect
allowHeaders reflect
exposeHeaders none
allowCredentials true
maxAgeSeconds null
也就是说,默认情况下:
Origin,默认把该 Origin 原样写回 Access-Control-Allow-Origin。Access-Control-Allow-Credentials: true。Access-Control-Max-Age。需要注意,默认配置会反射请求 Origin。生产环境如果只允许指定域名,应显式配置 allowOrigin(AllowOrigin.of(...));如果要完全禁止 CORS,应使用 AllowOrigin.ofNone()。
允许指定 Origin:
import dev.scx.http.routing.x.cors.CorsHandler;
import dev.scx.http.routing.x.cors.allow_origin.AllowOrigin;
router.route(
-10000,
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
);
允许多个 Origin:
router.route(
-10000,
CorsHandler.of()
.allowOrigin(AllowOrigin.of(
"https://a.example.com",
"https://b.example.com"
))
);
允许任意 Origin:
router.route(
-10000,
CorsHandler.of()
.allowOrigin(AllowOrigin.ofWildcard())
);
允许 credentials:
router.route(
-10000,
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
.allowCredentials(true)
);
需要注意,allowCredentials(true) 不能和任何 wildcard 策略组合使用。
CorsHandler 的执行流程大致如下:
1. 读取请求头 Origin
2. 如果 Origin 不存在,说明不是 CORS 请求,直接 context.next()
3. 使用 allowOrigin 校验 Origin
4. 如果 Origin 不允许,直接 context.next()
5. 写入 Access-Control-Allow-Origin
6. 写入 Vary: Origin
7. 如果 allowCredentials=true,写入 Access-Control-Allow-Credentials: true
8. 判断是否是预检请求
9. 预检请求直接返回 204
10. 非预检请求写入 Access-Control-Expose-Headers 后继续 context.next()
预检请求的判断条件是:
请求方法是 OPTIONS
并且存在 Access-Control-Request-Method 请求头
也就是说:
OPTIONS + Access-Control-Request-Method
才会被当作 CORS preflight。
如果请求中没有:
Origin
CorsHandler 不会修改响应,也不会结束请求。
它会直接调用:
context.next();
这意味着普通同源请求不会受到 CORS handler 的影响。
如果请求中存在 Origin,但 allowOrigin 返回 null,表示该 Origin 不允许。
此时 CorsHandler 不会返回错误,也不会设置 CORS 头。
它会继续:
context.next();
也就是说,是否因为没有 CORS 响应头而被浏览器拦截,是浏览器侧行为,不是服务端主动返回 403。
对于预检请求,CorsHandler 会直接生成响应。
示例请求:
OPTIONS /api/user HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
如果配置允许,则响应类似:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Vary: Origin
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, Authorization
如果设置了 maxAgeSeconds:
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
.maxAgeSeconds(3600L);
则会额外设置:
Access-Control-Max-Age: 3600
预检请求处理完成后不会继续调用 context.next()。
对于普通 CORS 请求,CorsHandler 会设置基础 CORS 响应头,然后继续路由链。
Access-Control-Allow-Origin
Vary
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
例如:
import dev.scx.http.routing.x.cors.expose_headers.ExposeHeaders;
import static dev.scx.http.headers.HttpHeaderName.CONTENT_RANGE;
import static dev.scx.http.headers.HttpHeaderName.ETAG;
router.route(
-10000,
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
.exposeHeaders(ExposeHeaders.of(ETAG, CONTENT_RANGE))
);
响应会包含:
Access-Control-Expose-Headers: ETag, Content-Range
然后继续:
context.next();
AllowOrigin 用于决定请求中的 Origin 是否允许。
接口可以理解为:
public sealed interface AllowOrigin {
static ListAllowOrigin of(String... origins)
static WildcardAllowOrigin ofWildcard()
static NoneAllowOrigin ofNone()
String allowedOrigin(String origin);
}
返回值语义:
返回 String 表示允许,并把该值写入 Access-Control-Allow-Origin
返回 null 表示不允许
import dev.scx.http.routing.x.cors.allow_origin.AllowOrigin;
var allowOrigin = AllowOrigin.of(
"https://a.example.com",
"https://b.example.com"
);
匹配规则是精确匹配。
Origin: https://a.example.com 允许
Origin: https://b.example.com 允许
Origin: https://c.example.com 不允许
ListAllowOrigin 会:
null 数组。null 元素。"*"。如果想使用通配符,应该使用:
AllowOrigin.ofWildcard()
而不是:
AllowOrigin.of("*")
var allowOrigin = AllowOrigin.ofWildcard();
它总是返回:
*
因此响应头是:
Access-Control-Allow-Origin: *
var allowOrigin = AllowOrigin.ofNone();
它总是返回:
null
如果要让某个 CorsHandler 完全不放行跨域请求,可以把 allowOrigin 设置为这个策略。
AllowMethods 用于决定预检响应中的:
Access-Control-Allow-Methods
接口可以理解为:
public sealed interface AllowMethods {
static ListAllowMethods of(ScxHttpMethod... methods)
static ReflectAllowMethods ofReflect()
static WildcardAllowMethods ofWildcard()
String allowedMethods(String requestMethodString);
}
import dev.scx.http.routing.x.cors.allow_methods.AllowMethods;
import static dev.scx.http.method.HttpMethod.GET;
import static dev.scx.http.method.HttpMethod.POST;
var allowMethods = AllowMethods.of(GET, POST);
生成结果类似:
GET, POST
ListAllowMethods 会:
null 数组。null method。"*"。, 的 method。", " 拼接。需要注意,ListAllowMethods 不会校验请求中的 Access-Control-Request-Method 是否在列表中。
它只返回配置好的 methods 字符串。
var allowMethods = AllowMethods.ofReflect();
它会直接返回请求中的:
Access-Control-Request-Method
例如请求头是:
Access-Control-Request-Method: POST
则响应头是:
Access-Control-Allow-Methods: POST
这是默认配置。
var allowMethods = AllowMethods.ofWildcard();
它返回:
*
因此响应头是:
Access-Control-Allow-Methods: *
AllowHeaders 用于决定预检响应中的:
Access-Control-Allow-Headers
接口可以理解为:
public sealed interface AllowHeaders {
static ListAllowHeaders of(ScxHttpHeaderName... headerNames)
static ReflectAllowHeaders ofReflect()
static WildcardAllowHeaders ofWildcard()
String allowedHeaders(String requestHeadersString);
}
返回值语义:
返回 String 写入 Access-Control-Allow-Headers
返回 null 不写入 Access-Control-Allow-Headers
import dev.scx.http.routing.x.cors.allow_headers.AllowHeaders;
import static dev.scx.http.headers.HttpHeaderName.AUTHORIZATION;
import static dev.scx.http.headers.HttpHeaderName.CONTENT_TYPE;
var allowHeaders = AllowHeaders.of(CONTENT_TYPE, AUTHORIZATION);
生成结果类似:
Content-Type, Authorization
ListAllowHeaders 会:
null 数组。null header。"*"。, 的 header。", " 拼接。var allowHeaders = AllowHeaders.ofReflect();
它会直接返回请求中的:
Access-Control-Request-Headers
例如请求头是:
Access-Control-Request-Headers: Content-Type, Authorization
则响应头是:
Access-Control-Allow-Headers: Content-Type, Authorization
这是默认配置。
如果请求中没有 Access-Control-Request-Headers,反射策略会返回 null,此时不会设置 Access-Control-Allow-Headers。
var allowHeaders = AllowHeaders.ofWildcard();
它返回:
*
因此响应头是:
Access-Control-Allow-Headers: *
ExposeHeaders 用于决定普通 CORS 响应中的:
Access-Control-Expose-Headers
接口可以理解为:
public sealed interface ExposeHeaders {
static ListExposeHeaders of(ScxHttpHeaderName... headerNames)
static WildcardExposeHeaders ofWildcard()
static NoneExposeHeaders ofNone()
String exposedHeaders();
}
返回值语义:
返回 String 写入 Access-Control-Expose-Headers
返回 null 不写入 Access-Control-Expose-Headers
import dev.scx.http.routing.x.cors.expose_headers.ExposeHeaders;
import static dev.scx.http.headers.HttpHeaderName.CONTENT_RANGE;
import static dev.scx.http.headers.HttpHeaderName.ETAG;
var exposeHeaders = ExposeHeaders.of(ETAG, CONTENT_RANGE);
生成结果类似:
ETag, Content-Range
ListExposeHeaders 的校验规则和 ListAllowHeaders 类似。
var exposeHeaders = ExposeHeaders.ofNone();
它返回:
null
因此不会设置:
Access-Control-Expose-Headers
这是默认配置。
var exposeHeaders = ExposeHeaders.ofWildcard();
它返回:
*
因此响应头是:
Access-Control-Expose-Headers: *
allowCredentials(true) 会让响应包含:
Access-Control-Allow-Credentials: true
示例:
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
.allowCredentials(true);
需要注意,当前实现禁止下面这些组合:
allowCredentials=true + WildcardAllowOrigin
allowCredentials=true + WildcardAllowMethods
allowCredentials=true + WildcardAllowHeaders
allowCredentials=true + WildcardExposeHeaders
如果这样配置,会抛出 IllegalArgumentException。
例如:
CorsHandler.of()
.allowOrigin(AllowOrigin.ofWildcard())
.allowCredentials(true);
这是非法配置。
推荐写法是显式列出允许的 Origin:
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
.allowCredentials(true);
maxAgeSeconds(...) 用于设置预检响应中的:
Access-Control-Max-Age
示例:
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
.maxAgeSeconds(3600L);
预检响应会包含:
Access-Control-Max-Age: 3600
如果传入 null,表示不设置该响应头。
如果传入小于 0 的值,会抛出 IllegalArgumentException。
import dev.scx.http.routing.Router;
import dev.scx.http.routing.x.cors.CorsHandler;
import dev.scx.http.routing.x.cors.allow_headers.AllowHeaders;
import dev.scx.http.routing.x.cors.allow_methods.AllowMethods;
import dev.scx.http.routing.x.cors.allow_origin.AllowOrigin;
import dev.scx.http.routing.x.cors.expose_headers.ExposeHeaders;
import static dev.scx.http.headers.HttpHeaderName.AUTHORIZATION;
import static dev.scx.http.headers.HttpHeaderName.CONTENT_RANGE;
import static dev.scx.http.headers.HttpHeaderName.CONTENT_TYPE;
import static dev.scx.http.headers.HttpHeaderName.ETAG;
import static dev.scx.http.method.HttpMethod.DELETE;
import static dev.scx.http.method.HttpMethod.GET;
import static dev.scx.http.method.HttpMethod.POST;
import static dev.scx.http.method.HttpMethod.PUT;
var router = Router.of();
router.route(
-10000,
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
.allowMethods(AllowMethods.of(GET, POST, PUT, DELETE))
.allowHeaders(AllowHeaders.of(CONTENT_TYPE, AUTHORIZATION))
.exposeHeaders(ExposeHeaders.of(ETAG, CONTENT_RANGE))
.allowCredentials(true)
.maxAgeSeconds(3600L)
);
StaticFilesHandler 是目录型静态文件处理器。
创建方式:
import dev.scx.http.routing.x.static_files.StaticFilesHandler;
import java.nio.file.Path;
var handler = StaticFilesHandler.of(Path.of("./public"));
挂载方式:
router.route(
"/*",
StaticFilesHandler.of(Path.of("./public"))
);
或者挂载到某个前缀下:
router.route(
"/assets/*",
StaticFilesHandler.of(Path.of("./assets"))
);
需要注意,StaticFilesHandler 依赖当前路由提供 * 捕获。
因此它应该挂载在:
/*
/assets/*
/public/*
这类带尾部通配符的路径模板上。
如果当前路由没有 * 捕获,运行时会抛出:
IllegalStateException
StaticFilesHandler 的处理流程大致如下:
1. 只处理 GET 和 HEAD
2. 非 GET / HEAD 直接 context.next()
3. 读取 pathMatch 中的 * 捕获
4. 把捕获转换为相对路径
5. root.resolve(relativePath).normalize()
6. 检查目标路径是否仍然在 root 内
7. 读取文件属性
8. 如果是常规文件,发送文件
9. 如果是目录且 URL 没有 / 结尾,重定向到带 / 的路径
10. 如果是目录且有 index.html,发送 index.html
11. 找不到文件则 context.next()
StaticFilesHandler 会把 root 转换为:
root.toAbsolutePath().normalize()
请求路径也会经过:
root.resolve(relativePath).normalize()
然后检查:
target.startsWith(root)
如果目标路径不在 root 内,会直接抛出:
NotFoundException
不会继续 context.next()。
这可以阻止类似下面这种路径逃逸:
/../../etc/passwd
如果目标路径是目录,StaticFilesHandler 会尝试返回该目录下的:
index.html
例如:
public/docs/index.html
请求:
/docs/
会尝试返回:
public/docs/index.html
如果 index.html 不存在,则继续:
context.next();
如果目标路径是目录,但请求路径没有以 / 结尾,会返回永久重定向。
例如:
/docs
如果 public/docs 是目录,则响应会设置:
Location: /docs/
状态码是:
308 Permanent Redirect
这样可以避免目录下相对资源加载错误。
StaticFilesHandler 支持设置 Cache-Control。
import dev.scx.http.routing.x.static_files.cache_control.CacheControl;
router.route(
"/*",
StaticFilesHandler.of(Path.of("./public"))
.cacheControl(CacheControl.of("public", "max-age=3600"))
);
响应会包含:
Cache-Control: public, max-age=3600
如果不设置 cacheControl,则不会写入 Cache-Control 响应头。
SingleFileHandler 是单文件处理器。
它和 StaticFilesHandler 的区别是:
StaticFilesHandler 根据请求路径从目录中查找文件
SingleFileHandler 总是尝试返回同一个文件
创建方式:
import dev.scx.http.routing.x.single_file.SingleFileHandler;
import java.nio.file.Path;
var handler = SingleFileHandler.of(Path.of("./public/index.html"));
挂载方式:
router.route(
"/*",
SingleFileHandler.of(Path.of("./public/index.html"))
);
常用于 SPA fallback。
SingleFileHandler 的处理流程大致如下:
1. 只处理 GET 和 HEAD
2. 非 GET / HEAD 直接 context.next()
3. 读取构造时传入的固定文件
4. 如果文件不存在或不是常规文件,context.next()
5. 如果设置了 cacheControl,写入 Cache-Control
6. 使用 StaticFilesSupport.serveFile(...) 发送文件
需要注意,SingleFileHandler 不会根据请求路径查找文件。
只要路由匹配,它就会尝试返回构造时指定的那个文件。
常见挂载顺序是:
router.route("/api/hello", ctx -> {
ctx.request().response().send("hello");
});
router.route(
"/*",
StaticFilesHandler.of(Path.of("./public"))
);
router.route(
"/*",
SingleFileHandler.of(Path.of("./public/index.html"))
);
大致语义是:
/api/hello 先走 API
/assets/app.js 静态文件存在则返回静态文件
/dashboard 静态文件不存在,最后返回 index.html
这样可以支持前端路由。
StaticFilesSupport 是静态文件响应工具类。
它提供两类主要方法:
sendFile(...)
serveFile(...)
区别是:
sendFile 处理文件发送和 Range
serveFile 在 sendFile 基础上增加 ETag / Last-Modified / 304
sendFile(...) 用于发送文件,并处理 Range 请求。
StaticFilesSupport.sendFile(file, request);
它会:
Accept-Ranges: bytes。Range 请求头,发送完整文件。Range 请求头,解析 Range。416 Range Not Satisfiable。416 Range Not Satisfiable。206 Partial Content。Content-Range。serveFile(...) 用于发送带缓存验证能力的文件。
StaticFilesSupport.serveFile(file, request);
它会:
ETag。Last-Modified。If-None-Match。If-Modified-Since。304 Not Modified。sendFile(...)。也就是说:
serveFile = HTTP 缓存验证 + sendFile
如果只是文件下载,不关心浏览器缓存:
StaticFilesSupport.sendFile(file, request);
如果希望浏览器能使用缓存验证:
StaticFilesSupport.serveFile(file, request);
StaticFilesHandler 和 SingleFileHandler 内部使用的是:
StaticFilesSupport.serveFile(...)
因此它们默认支持:
ETag
Last-Modified
If-None-Match
If-Modified-Since
304 Not Modified
Range
206 Partial Content
416 Range Not Satisfiable
StaticFilesSupport 创建的 ETag 格式是:
"size-lastModifiedMillis"
例如:
"12345-1710000000000"
其中:
size 文件大小
lastModifiedMillis 文件最后修改时间的毫秒值
响应会包含:
ETag: "12345-1710000000000"
如果请求头:
If-None-Match: "12345-1710000000000"
和当前 ETag 完全相等,则返回:
304 Not Modified
StaticFilesSupport 会根据文件最后修改时间设置:
Last-Modified
时间会按 HTTP-date 格式输出,并截断到秒。
例如:
Last-Modified: Fri, 08 May 2026 12:30:00 GMT
如果请求中没有 If-None-Match,但存在:
If-Modified-Since
则会解析该时间。
如果文件最后修改时间不晚于 If-Modified-Since,则返回:
304 Not Modified
如果 If-Modified-Since 解析失败,则认为没有有效缓存条件,继续发送文件。
缓存验证时,If-None-Match 优先于 If-Modified-Since。
也就是说:
如果存在 If-None-Match,则只比较 ETag
如果不存在 If-None-Match,但存在 If-Modified-Since,则比较 Last-Modified
这和当前实现的判断顺序一致。
Range 表示请求头:
Range: bytes=...
支持三种形式:
bytes=start-end
bytes=start-
bytes=-suffix
示例:
import dev.scx.http.routing.x.static_files.range.Range;
var r1 = Range.parse("bytes=0-499");
var r2 = Range.parse("bytes=500-");
var r3 = Range.parse("bytes=-500");
编码:
System.out.println(r1.encode());
System.out.println(r2.encode());
System.out.println(r3.encode());
输出:
bytes=0-499
bytes=500-
bytes=-500
Range.parse(...) 的规则包括:
bytes= 大小写不敏感。start 和 end 两侧可以有空白。start 和 end 不能同时为空。start 不能小于 0。end 不能小于 0。start 和 end 都存在,则 start <= end。IllegalRangeException。合法示例:
bytes=0-499
bytes=500-
bytes=-500
bytes=0-499
BYTES=0-499
bytes=0 - 499
bytes=0-1,4-5
其中多段 Range:
bytes=0-1,4-5
只取第一段:
bytes=0-1
非法示例:
bytes
bytes 0-499
items=0-10
bytes=0
bytes=-
bytes=--500
bytes=abc-def
bytes=500-499
ContentRange 表示响应头:
Content-Range
支持两种形式:
bytes start-end/size
bytes */size
普通范围:
import dev.scx.http.routing.x.static_files.content_range.ContentRange;
var contentRange = ContentRange.of(0, 499, 1000);
System.out.println(contentRange.encode());
输出:
bytes 0-499/1000
不满足范围:
var contentRange = ContentRange.ofUnsatisfied(1000);
System.out.println(contentRange.encode());
输出:
bytes */1000
ContentRange 会校验:
size >= 0
普通范围时 start 和 end 都不能为 null
普通范围时 start >= 0
普通范围时 end >= 0
普通范围时 start <= end
普通范围时 end < size
不满足范围时 start 和 end 都必须为 null
如果不满足,会抛出:
IllegalArgumentException
解析失败时会抛出:
IllegalContentRangeException
StaticFilesSupport.resolveContentRange(...) 用于把请求中的 Range 转成可响应的 ContentRange。
假设文件大小是:
1000
请求:
bytes=0-499
结果:
bytes 0-499/1000
请求:
bytes=500-
结果:
bytes 500-999/1000
请求:
bytes=-100
结果:
bytes 900-999/1000
请求:
bytes=2000-
结果:
bytes */1000
也就是范围不满足。
如果请求:
bytes=0-999999
但文件大小只有:
1000
则实际范围会被裁剪为:
bytes 0-999/1000
如果请求:
bytes=-2000
但文件大小只有:
1000
则返回整个文件范围:
bytes 0-999/1000
如果文件大小是:
0
任何 Range 都会被视为不满足:
bytes */0
当 Range 合法且可以满足时,sendFile(...) 会返回:
206 Partial Content
Content-Range: bytes start-end/size
Accept-Ranges: bytes
然后只发送文件中对应区间的数据。
这对视频拖动、断点续传、浏览器分段加载等场景很重要。
当 Range 解析失败或范围不满足时,sendFile(...) 会返回:
416 Range Not Satisfiable
Content-Range: bytes */size
例如文件大小是 1000,请求:
Range: bytes=2000-
响应会包含:
Content-Range: bytes */1000
CacheControl 用于构造 Cache-Control 响应头。
创建方式:
import dev.scx.http.routing.x.static_files.cache_control.CacheControl;
var cacheControl = CacheControl.of("public", "max-age=3600");
编码结果:
System.out.println(cacheControl.encode());
输出:
public, max-age=3600
CacheControl.of(...) 会:
null 数组。null directive。, 的 directive。", " 拼接。示例:
var cacheControl = CacheControl.of(
" public ",
"max-age=3600",
"public"
);
结果:
public, max-age=3600
短缓存:
CacheControl.of("public", "max-age=60")
长期缓存:
CacheControl.of("public", "max-age=31536000", "immutable")
禁止缓存:
CacheControl.of("no-store")
每次使用前重新验证:
CacheControl.of("no-cache")
静态文件处理器中使用:
router.route(
"/assets/*",
StaticFilesHandler.of(Path.of("./assets"))
.cacheControl(CacheControl.of("public", "max-age=31536000", "immutable"))
);
HttpDateHelper 用于 HTTP-date 编解码。
解析:
import dev.scx.http.routing.x.static_files.http_date.HttpDateHelper;
var instant = HttpDateHelper.parse("Fri, 08 May 2026 12:30:00 GMT");
编码:
var value = HttpDateHelper.encode(instant);
编码时会:
解析失败时会抛出:
IllegalHttpDateException
HttpValidator 是一个简单 record:
public record HttpValidator(
String etag,
Instant lastModified
) {
}
StaticFilesSupport.createValidator(...) 会根据文件属性创建它。
var validator = StaticFilesSupport.createValidator(attr);
然后用于判断:
StaticFilesSupport.checkNotModified(validator, request);
StaticFilesHandler 和 SingleFileHandler 都只处理:
GET
HEAD
如果请求方法不是 GET 或 HEAD,它们会直接:
context.next();
因此它们不会拦截 POST、PUT、DELETE 等请求。
StaticFilesHandler 和 SingleFileHandler 会把 HEAD 请求交给 StaticFilesSupport.serveFile(...)。
具体是否发送响应体,由底层 ScxHttpServerResponse#send(...) 对 HEAD 的处理语义决定。
在 handler 层,它和 GET 一样会走静态文件定位、缓存验证和 Range 处理流程。
在发送完整文件或 Range 文件时,如果底层写出阶段抛出的包装异常原因是:
ScxOutputException
StaticFilesSupport 会直接返回,不再继续向外抛出。
这是为了处理浏览器在视频拖动、跳转、取消下载时主动中断连接的场景。
其它异常仍然会继续抛出。
import dev.scx.http.routing.Router;
import dev.scx.http.routing.x.cors.CorsHandler;
import dev.scx.http.routing.x.cors.allow_origin.AllowOrigin;
import dev.scx.http.routing.x.static_files.StaticFilesHandler;
import dev.scx.http.routing.x.static_files.cache_control.CacheControl;
import java.nio.file.Path;
var router = Router.of();
router.route(
-10000,
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"))
);
router.route(
"/assets/*",
StaticFilesHandler.of(Path.of("./assets"))
.cacheControl(CacheControl.of(
"public",
"max-age=31536000",
"immutable"
))
);
import dev.scx.http.routing.Router;
import dev.scx.http.routing.x.static_files.StaticFilesSupport;
import java.io.File;
var router = Router.of();
router.route("/download", ctx -> {
var file = new File("./files/demo.zip");
StaticFilesSupport.sendFile(file, ctx.request());
});
这个示例会支持:
完整文件响应
Range 请求
206 Partial Content
416 Range Not Satisfiable
Accept-Ranges: bytes
但不会自动加入:
ETag
Last-Modified
304 Not Modified
如果需要这些能力,应使用:
StaticFilesSupport.serveFile(file, ctx.request());
import dev.scx.http.routing.Router;
import dev.scx.http.routing.x.static_files.StaticFilesSupport;
import java.io.File;
var router = Router.of();
router.route("/image", ctx -> {
var file = new File("./files/image.png");
StaticFilesSupport.serveFile(file, ctx.request());
});
这个示例会支持:
ETag
Last-Modified
If-None-Match
If-Modified-Since
304 Not Modified
Range
206 Partial Content
416 Range Not Satisfiable
import dev.scx.http.routing.Router;
import dev.scx.http.routing.x.single_file.SingleFileHandler;
import dev.scx.http.routing.x.static_files.StaticFilesHandler;
import dev.scx.http.routing.x.static_files.cache_control.CacheControl;
import java.nio.file.Path;
var router = Router.of();
router.route("/api/hello", ctx -> {
ctx.request().response().send("hello");
});
router.route(
"/assets/*",
StaticFilesHandler.of(Path.of("./public/assets"))
.cacheControl(CacheControl.of(
"public",
"max-age=31536000",
"immutable"
))
);
router.route(
"/*",
StaticFilesHandler.of(Path.of("./public"))
);
router.route(
"/*",
SingleFileHandler.of(Path.of("./public/index.html"))
.cacheControl(CacheControl.of("no-cache"))
);
这个路由组合适合:
/api/hello API
/assets/app.js 静态资源,长期缓存
/favicon.ico 普通静态文件
/dashboard 前端路由,返回 index.html
/settings/profile 前端路由,返回 index.html
static CorsHandler of()
CorsHandler allowOrigin(AllowOrigin allowOrigin)
CorsHandler allowMethods(AllowMethods allowMethods)
CorsHandler allowHeaders(AllowHeaders allowHeaders)
CorsHandler exposeHeaders(ExposeHeaders exposeHeaders)
CorsHandler allowCredentials(boolean allowCredentials)
CorsHandler maxAgeSeconds(Long maxAgeSeconds)
static ListAllowOrigin of(String... origins)
static WildcardAllowOrigin ofWildcard()
static NoneAllowOrigin ofNone()
String allowedOrigin(String origin)
static ListAllowMethods of(ScxHttpMethod... methods)
static ReflectAllowMethods ofReflect()
static WildcardAllowMethods ofWildcard()
String allowedMethods(String requestMethodString)
static ListAllowHeaders of(ScxHttpHeaderName... headerNames)
static ReflectAllowHeaders ofReflect()
static WildcardAllowHeaders ofWildcard()
String allowedHeaders(String requestHeadersString)
static ListExposeHeaders of(ScxHttpHeaderName... headerNames)
static WildcardExposeHeaders ofWildcard()
static NoneExposeHeaders ofNone()
String exposedHeaders()
static StaticFilesHandler of(Path root)
StaticFilesHandler cacheControl(CacheControl cacheControl)
static SingleFileHandler of(Path file)
SingleFileHandler cacheControl(CacheControl cacheControl)
static void sendFile(
File target,
ScxHttpServerRequest request
)
static void sendFile(
File target,
BasicFileAttributes attr,
ScxHttpServerRequest request
)
static void serveFile(
File target,
ScxHttpServerRequest request
)
static void serveFile(
File target,
BasicFileAttributes attr,
ScxHttpServerRequest request
)
static ContentRange resolveContentRange(
Range range,
long size
)
static HttpValidator createValidator(
BasicFileAttributes attr
)
static boolean checkNotModified(
HttpValidator httpValidator,
ScxHttpServerRequest request
)
static CacheControl of(String... directives)
String encode()
new Range(Long start, Long end)
static Range parse(String rangeStr)
String encode()
new ContentRange(Long start, Long end, Long size)
static ContentRange of(long start, long end, long size)
static ContentRange ofUnsatisfied(long size)
static ContentRange parse(String contentRangeStr)
boolean isUnsatisfied()
String encode()
static Instant parse(String v)
static String encode(Instant instant)
public record HttpValidator(
String etag,
Instant lastModified
) {
}
scx-http-routing 提供路由匹配、Router、RoutingContext 等核心能力。
scx-http-routing-x 提供的是可以挂载到路由上的常用 handler。
也就是说:
scx-http-routing 负责路由
scx-http-routing-x 负责常用扩展处理器
CorsHandler、StaticFilesHandler、SingleFileHandler 都可以直接作为 routing handler 使用。
因为它们都实现了:
Function1Void<RoutingContext, Throwable>
所以可以这样挂载:
router.route("/*", StaticFilesHandler.of(Path.of("./public")));
当 Origin 不允许时,CorsHandler 不会主动返回 403。
它只是不给响应添加 CORS 头,并继续路由链。
真正的跨域阻断通常发生在浏览器端。
如果请求是 CORS 预检请求,并且 Origin 允许,CorsHandler 会直接返回:
204
不会继续调用 context.next()。
当前实现明确禁止:
allowCredentials=true
和任何 wildcard 策略组合。
这可以避免产生容易误用的跨域响应配置。
StaticFilesHandler 不直接读取完整 path 来映射文件,而是依赖当前路由模板中的 * 捕获。
这让它可以自然挂载在不同前缀下。
router.route("/assets/*", StaticFilesHandler.of(Path.of("./assets")));
静态文件处理器会检查最终目标路径是否仍然位于 root 下。
越界请求直接抛出 NotFoundException。
这是静态文件服务中非常重要的安全边界。
SingleFileHandler 不根据请求路径查找文件。
它只返回一个固定文件。
因此它适合放在路由链最后,用来处理前端路由。
sendFile(...) 负责文件发送和 Range。
serveFile(...) 负责 ETag / Last-Modified / 304,然后再调用 sendFile(...)。
使用静态文件 handler 时,内部用的是 serveFile(...)。
当前 Range.parse(...) 支持多段格式,但只取第一段。
例如:
bytes=0-1,4-5
会被解析为:
bytes=0-1
它不会返回 multipart/byteranges 响应。
HttpDateHelper 使用 DateTimeFormatter.RFC_1123_DATE_TIME 解析和编码 HTTP-date。
编码时使用 UTC,并截断到秒。
CacheControl 不理解每个 directive 的具体 HTTP 语义。
它只负责:
校验 directive
去重
拼接成 Cache-Control 头值
例如:
CacheControl.of("public", "max-age=3600")
生成:
public, max-age=3600
不是。
它只是 scx-http-routing 的扩展 handler 库。
HTTP Server 能力来自 scx-http / scx-http-x 等模块。
/* 这种路由上?因为它需要从 RoutingContext#pathMatch() 中读取 * 捕获。
如果路由没有提供 * 捕获,它不知道应该把哪一段路径映射到文件系统中。
会调用:
context.next();
也就是说,它会把请求交给后续路由继续处理。
不会。
如果解析后的目标路径不在 root 内,会直接抛出 NotFoundException。
这是为了防止路径穿越攻击。
不会。
它只处理:
GET
HEAD
其它方法直接 context.next()。
/ 结尾?为了避免目录下相对资源路径解析错误。
例如请求:
/docs
会重定向到:
/docs/
会调用:
context.next();
StaticFilesHandler 根据请求路径在目录中找文件。
SingleFileHandler 总是尝试返回同一个固定文件。
会调用:
context.next();
sendFile(...) 处理文件发送和 Range。
serveFile(...) 在 sendFile(...) 前增加 ETag / Last-Modified / 304 处理。
支持。
StaticFilesHandler 和 SingleFileHandler 内部都调用 StaticFilesSupport.serveFile(...),而 serveFile(...) 会继续调用支持 Range 的 sendFile(...)。
返回:
416 Range Not Satisfiable
并设置:
Content-Range: bytes */size
不会。
当前实现只取第一段 Range。
支持。
serveFile(...) 会根据文件大小和最后修改时间生成 ETag。
支持。
serveFile(...) 会设置 Last-Modified。
If-None-Match 优先。
如果存在 If-None-Match,就不会再使用 If-Modified-Since 判断。
不会。
只有调用:
cacheControl(...)
之后才会设置 Cache-Control。
不可以。
每个 directive 不能包含 ,。
应该这样传:
CacheControl.of("public", "max-age=3600")
而不是:
CacheControl.of("public, max-age=3600")
是。当前 CorsHandler.of() 默认使用 reflect origin,并启用 credentials。
也就是说,请求里有 Origin 时,默认会把这个 Origin 写回 Access-Control-Allow-Origin,并写入 Access-Control-Allow-Credentials: true。
生产环境通常应该显式收窄:
CorsHandler.of()
.allowOrigin(AllowOrigin.of("https://example.com"));
默认是 reflect。
也就是把请求中的 Access-Control-Request-Method 原样写回 Access-Control-Allow-Methods。
默认是 reflect。
也就是把请求中的 Access-Control-Request-Headers 原样写回 Access-Control-Allow-Headers。
如果请求没有这个头,则不写 Access-Control-Allow-Headers。
默认是 none。
也就是不设置 Access-Control-Expose-Headers。
不可以。
当前实现会直接抛出 IllegalArgumentException。
不会。
CorsHandler 会继续 context.next(),但不会添加 CORS 响应头。
浏览器会根据缺失的 CORS 响应头自行拦截。
因为 CORS 响应头通常应该尽早设置,尤其是预检请求需要尽早处理并直接返回 204。
示例:
router.route(-10000, CorsHandler.of().allowOrigin(...));
适合下面这些场景:
scx-http-routing 增加 CORS。