feat(service): 增加动态站点路由能力(by AI)
This commit is contained in:
parent
0a3fe19a56
commit
c412e551c4
@ -1,5 +1,11 @@
|
||||
# CHANGELOG - go/service
|
||||
|
||||
## v1.5.24 (2026-08-22)
|
||||
- **动态静态源**: 新增 `StaticFS` 与 `ReplaceStaticFS`,可以按 Host 原子替换自定义 `http.FileSystem`;增加 `ReplaceStaticFSExclusive`,支持选中虚拟主机后隔离后续 Host 静态源。
|
||||
- **Host 一致性**: Static 与 Service 共用 `host:port → host → :port → *` 候选顺序。
|
||||
- **流式静态响应**: 自定义静态源通过 `http.ServeContent` 支持 Range、HEAD 和大文件流式读取;非开发模式静态内容默认绕过 output filter。
|
||||
- **路由兜底**: 新增 `SetFallback`,在静态文件和注册路由均未命中时进入统一处理链。
|
||||
|
||||
## v1.5.23 (2026-08-17)
|
||||
- **流式响应**: `Response.Write` 保持直接写入和实时 Flush,不再因服务注册了输出过滤器而被统一缓冲。
|
||||
- **显式过滤**: 新增 `Response.WriteFiltered`,静态文件等确实需要输出过滤器处理的内容可显式进入缓冲流程。
|
||||
|
||||
@ -75,6 +75,8 @@ func main() {
|
||||
|
||||
### 4. 增强插件
|
||||
- **静态文件**: `service.Static("/ui", "./static_dir")` 或 `service.Host("example.com").Static("/ui", "./static_dir")`
|
||||
- **自定义静态源**: `service.Host("example.com").StaticFS("/", source)` 或 `service.ReplaceStaticFS(host, mounts)`,适用于知识库、对象存储和虚拟文件系统。需要虚拟主机隔离时使用 `service.ReplaceStaticFSExclusive(host, mounts)`;选中该 Host 后,文件未命中不会继续进入后续 Host 的自定义静态源。
|
||||
- **未命中处理**: `service.SetFallback(handler)` 在静态文件和已注册路由都未命中后执行。
|
||||
- **URL 重写**: `service.Rewrite("/old", "/new")`
|
||||
- **反向代理**: `service.Proxy(0, "/api", "other_app", "/api")`
|
||||
- **文档生成**: `service.MakeDocument()` 返回全量接口描述
|
||||
@ -82,6 +84,8 @@ func main() {
|
||||
|
||||
低代码需要转发二进制或 SSE 流时,使用 `response.WriteBytes(chunk)` 原样写入动态运行时提供的字节数组;文本输出继续使用 `response.WriteString(text)`。`Write`、`WriteBytes` 和 `WriteString` 都直接写入响应并保持流式行为,不会被全局输出过滤器强制缓冲;确实需要过滤器处理的内容应显式使用 `response.WriteFiltered(bytes)`。
|
||||
|
||||
自定义静态源必须返回可 `Seek` 的 `http.File`。生产模式下静态内容直接流式输出并绕过 output filter;`EnableWebDev` 仅会为 HTML 显式缓冲内容,用于注入开发热刷新脚本。
|
||||
|
||||
## 配置指南 (ServiceConfig)
|
||||
|
||||
详细配置项可查阅 `config.go` 中的 `ServiceConfig` 结构。通过 `config.Load` 支持从 `env.yml` 或环境变量加载。
|
||||
|
||||
6
TEST.md
6
TEST.md
@ -1,5 +1,11 @@
|
||||
# Service Module Test Report
|
||||
|
||||
## v1.5.24 (2026-08-22)
|
||||
- `TestStaticFSHostCandidatesAndRange` 验证自定义静态源的四级 Host 优先级与 Range 206 响应。
|
||||
- `TestStaticFSExclusiveStopsFileFallback` 验证隔离 Host 文件未命中时返回 404,未匹配 Host 仍使用全局默认静态源。
|
||||
- `TestFallbackRunsOnlyAfterRegisteredRoutesMiss` 验证已注册路由优先于 Fallback。
|
||||
- `go test ./...` 通过。
|
||||
|
||||
## v1.5.23 验证
|
||||
- `TestResponseWriteBytes` 验证动态字节数组写入时保持 UTF-8 内容不变。
|
||||
- `TestResponseWriteBypassesOutputFilterBuffer` 验证直接响应即使存在输出过滤器也保持实时写入。
|
||||
|
||||
58
handler.go
58
handler.go
@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
@ -152,6 +153,11 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
s, wsc = ws.findService(r.Method, host, path, args)
|
||||
if s == nil && wsc == nil {
|
||||
ws.webServicesLock.RLock()
|
||||
s = ws.fallbackService
|
||||
ws.webServicesLock.RUnlock()
|
||||
}
|
||||
|
||||
// 4. 参数解析 (Form & Body)
|
||||
parseRequestArgs(request, args)
|
||||
@ -240,8 +246,49 @@ filter:
|
||||
}
|
||||
|
||||
func hostOnly(host string) string {
|
||||
h, _, _ := strings.Cut(host, ":")
|
||||
return h
|
||||
h, _, err := net.SplitHostPort(host)
|
||||
if err == nil {
|
||||
return strings.Trim(h, "[]")
|
||||
}
|
||||
if strings.Count(host, ":") == 1 {
|
||||
h, _, _ = strings.Cut(host, ":")
|
||||
return h
|
||||
}
|
||||
return strings.Trim(host, "[]")
|
||||
}
|
||||
|
||||
func hostCandidates(host string) []string {
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
hostName, port := hostOnly(host), ""
|
||||
if _, parsedPort, err := net.SplitHostPort(host); err == nil {
|
||||
port = parsedPort
|
||||
} else if strings.Count(host, ":") == 1 {
|
||||
_, port, _ = strings.Cut(host, ":")
|
||||
}
|
||||
hostName = strings.TrimSuffix(hostName, ".")
|
||||
normalizedHost := hostName
|
||||
if port != "" {
|
||||
normalizedHost = net.JoinHostPort(hostName, port)
|
||||
}
|
||||
candidates := []string{normalizedHost}
|
||||
if port != "" {
|
||||
candidates = append(candidates, hostName, ":"+port)
|
||||
}
|
||||
candidates = append(candidates, "*")
|
||||
out := candidates[:0]
|
||||
seen := map[string]bool{}
|
||||
for _, candidate := range candidates {
|
||||
if candidate != "" && !seen[candidate] {
|
||||
seen[candidate] = true
|
||||
out = append(out, candidate)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// HostCandidates returns route keys from most specific to the global fallback.
|
||||
func HostCandidates(host string) []string {
|
||||
return append([]string(nil), hostCandidates(host)...)
|
||||
}
|
||||
|
||||
func (ws *WebServer) findService(method, host, path string, args map[string]any) (*webServiceType, *websocketServiceType) {
|
||||
@ -249,12 +296,7 @@ func (ws *WebServer) findService(method, host, path string, args map[string]any)
|
||||
defer ws.webServicesLock.RUnlock()
|
||||
|
||||
// 1. 准备 Host 候选列表: "host:port", "host", ":port", "*"
|
||||
hostOnly, port, _ := strings.Cut(host, ":")
|
||||
hosts := []string{host}
|
||||
if port != "" {
|
||||
hosts = append(hosts, hostOnly, ":"+port)
|
||||
}
|
||||
hosts = append(hosts, "*")
|
||||
hosts := hostCandidates(host)
|
||||
|
||||
// 2. 匹配 Web Service
|
||||
for _, h := range hosts {
|
||||
|
||||
@ -81,3 +81,25 @@ func TestServeHTTP_Panic(t *testing.T) {
|
||||
t.Errorf("Expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackRunsOnlyAfterRegisteredRoutesMiss(t *testing.T) {
|
||||
ws := NewWebServer()
|
||||
ws.Host("*").GET("/registered", func() string { return "registered" })
|
||||
ws.SetFallback(func(request *Request) string { return "fallback:" + request.URL.Path })
|
||||
handler := &RouteHandler{ws: ws}
|
||||
|
||||
for _, test := range []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"/registered", "registered"},
|
||||
{"/missing", "fallback:/missing"},
|
||||
} {
|
||||
request := httptest.NewRequest(http.MethodGet, test.path, nil)
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || response.Body.String() != test.want {
|
||||
t.Fatalf("%s: got %d %q, want 200 %q", test.path, response.Code, response.Body.String(), test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ type WebServer struct {
|
||||
regexWebServices map[string][]*webServiceType
|
||||
webServicesLock sync.RWMutex
|
||||
webServicesList []*webServiceType
|
||||
fallbackService *webServiceType
|
||||
|
||||
websocketServices map[string]map[string]*websocketServiceType
|
||||
websocketServicesLock sync.RWMutex
|
||||
@ -73,6 +74,8 @@ type WebServer struct {
|
||||
dynamicStatics map[string]map[string]*string
|
||||
hostStatics map[string][]*staticType
|
||||
staticsByHostLock sync.RWMutex
|
||||
staticFSByHost map[string]map[string]http.FileSystem
|
||||
staticFSExclusive map[string]bool
|
||||
|
||||
// 过滤器与拦截器
|
||||
inFilters []func(*map[string]any, *Request, *Response, *log.Logger) any
|
||||
@ -141,6 +144,8 @@ func NewWebServer() *WebServer {
|
||||
fileStatics: make(map[string]map[string]*string),
|
||||
dynamicStatics: make(map[string]map[string]*string),
|
||||
hostStatics: make(map[string][]*staticType),
|
||||
staticFSByHost: make(map[string]map[string]http.FileSystem),
|
||||
staticFSExclusive: make(map[string]bool),
|
||||
webAuthCheckers: make(map[int]func(int, *log.Logger, *string, map[string]any, *Request, *Response, *WebServiceOptions) (pass bool, object any)),
|
||||
injectObjects: make(map[reflect.Type]any),
|
||||
injectFunctions: make(map[reflect.Type]func() any),
|
||||
|
||||
20
service.go
20
service.go
@ -155,6 +155,25 @@ func (ws *WebServer) Register(method, path string, serviceFunc any) *webServiceT
|
||||
return ws.Host("*").Register(method, path, serviceFunc)
|
||||
}
|
||||
|
||||
// SetFallback sets the handler used after static files and registered routes miss.
|
||||
func SetFallback(serviceFunc any) *webServiceType {
|
||||
return DefaultServer.SetFallback(serviceFunc)
|
||||
}
|
||||
|
||||
func (ws *WebServer) SetFallback(serviceFunc any) *webServiceType {
|
||||
s, err := makeCachedService(serviceFunc)
|
||||
if err != nil {
|
||||
return &webServiceType{}
|
||||
}
|
||||
s.host = "*"
|
||||
s.method = "*"
|
||||
s.path = "*"
|
||||
ws.webServicesLock.Lock()
|
||||
ws.fallbackService = s
|
||||
ws.webServicesLock.Unlock()
|
||||
return s
|
||||
}
|
||||
|
||||
// RegisterWebsocket 注册一个 WebSocket 服务 (使用默认 Host "*")
|
||||
func RegisterWebsocket(path string, serviceFunc any) *websocketServiceType {
|
||||
return DefaultServer.RegisterWebsocket(path, serviceFunc)
|
||||
@ -632,4 +651,3 @@ func (ws *WebServer) initWebDev(logger *log.Logger) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
202
static.go
202
static.go
@ -3,14 +3,22 @@ package service
|
||||
import (
|
||||
"apigo.cc/go/file"
|
||||
"apigo.cc/go/log"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
pathpkg "path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type staticFSMatch struct {
|
||||
fileSystem http.FileSystem
|
||||
name string
|
||||
}
|
||||
|
||||
// Static 注册静态文件目录
|
||||
func (hc *HostContext) Static(path, rootPath string) *HostContext {
|
||||
host := hc.host
|
||||
@ -30,6 +38,61 @@ func (ws *WebServer) Static(path, rootPath string) {
|
||||
ws.Host("*").Static(path, rootPath)
|
||||
}
|
||||
|
||||
// StaticFS registers a custom static file source for a URL prefix.
|
||||
func (hc *HostContext) StaticFS(path string, source http.FileSystem) *HostContext {
|
||||
host := hc.host
|
||||
if host == "*" {
|
||||
host = ""
|
||||
}
|
||||
hc.ws.staticsByHostLock.Lock()
|
||||
if hc.ws.staticFSByHost[host] == nil {
|
||||
hc.ws.staticFSByHost[host] = map[string]http.FileSystem{}
|
||||
}
|
||||
hc.ws.staticFSByHost[host][path] = source
|
||||
hc.ws.staticsByHostLock.Unlock()
|
||||
return hc
|
||||
}
|
||||
|
||||
// ReplaceStaticFS atomically replaces custom static file sources for a host.
|
||||
func ReplaceStaticFS(host string, config map[string]http.FileSystem) {
|
||||
DefaultServer.ReplaceStaticFS(host, config)
|
||||
}
|
||||
|
||||
func (ws *WebServer) ReplaceStaticFS(host string, config map[string]http.FileSystem) {
|
||||
ws.replaceStaticFS(host, config, false)
|
||||
}
|
||||
|
||||
// ReplaceStaticFSExclusive atomically replaces custom static file sources for
|
||||
// a host and stops custom StaticFS fallback after that host is selected.
|
||||
func ReplaceStaticFSExclusive(host string, config map[string]http.FileSystem) {
|
||||
DefaultServer.ReplaceStaticFSExclusive(host, config)
|
||||
}
|
||||
|
||||
func (ws *WebServer) ReplaceStaticFSExclusive(host string, config map[string]http.FileSystem) {
|
||||
ws.replaceStaticFS(host, config, true)
|
||||
}
|
||||
|
||||
func (ws *WebServer) replaceStaticFS(host string, config map[string]http.FileSystem, exclusive bool) {
|
||||
if host == "*" {
|
||||
host = ""
|
||||
}
|
||||
next := make(map[string]http.FileSystem, len(config))
|
||||
for route, source := range config {
|
||||
if source != nil {
|
||||
next[route] = source
|
||||
}
|
||||
}
|
||||
ws.staticsByHostLock.Lock()
|
||||
if len(next) == 0 {
|
||||
delete(ws.staticFSByHost, host)
|
||||
delete(ws.staticFSExclusive, host)
|
||||
} else {
|
||||
ws.staticFSByHost[host] = next
|
||||
ws.staticFSExclusive[host] = exclusive
|
||||
}
|
||||
ws.staticsByHostLock.Unlock()
|
||||
}
|
||||
|
||||
// StaticByHost 为指定域名注册静态文件目录
|
||||
func StaticByHost(path, rootPath, host string) {
|
||||
DefaultServer.StaticByHost(path, rootPath, host)
|
||||
@ -81,13 +144,48 @@ func (ws *WebServer) getStaticFilePath(requestPath, host string) string {
|
||||
ws.staticsByHostLock.RLock()
|
||||
defer ws.staticsByHostLock.RUnlock()
|
||||
|
||||
// 优先匹配指定域名的配置
|
||||
if filePath := ws.findMatchedPathSorted(ws.hostStatics[host], requestPath); filePath != "" {
|
||||
return filePath
|
||||
for _, candidate := range hostCandidates(host) {
|
||||
if candidate == "*" {
|
||||
candidate = ""
|
||||
}
|
||||
if filePath := ws.findMatchedPathSorted(ws.hostStatics[candidate], requestPath); filePath != "" {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 匹配全局配置
|
||||
return ws.findMatchedPathSorted(ws.hostStatics[""], requestPath)
|
||||
func (ws *WebServer) getStaticFSMatches(requestPath, host string) []staticFSMatch {
|
||||
requestPath, _ = url.PathUnescape(requestPath)
|
||||
ws.staticsByHostLock.RLock()
|
||||
defer ws.staticsByHostLock.RUnlock()
|
||||
matches := make([]staticFSMatch, 0)
|
||||
for _, candidate := range hostCandidates(host) {
|
||||
if candidate == "*" {
|
||||
candidate = ""
|
||||
}
|
||||
config, exists := ws.staticFSByHost[candidate]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
routes := make([]string, 0, len(config))
|
||||
for route := range config {
|
||||
routes = append(routes, route)
|
||||
}
|
||||
sort.Slice(routes, func(i, j int) bool { return len(routes[i]) > len(routes[j]) })
|
||||
for _, route := range routes {
|
||||
if !strings.HasPrefix(requestPath, route) {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimPrefix(requestPath, route)
|
||||
name = strings.TrimPrefix(pathpkg.Clean("/"+name), "/")
|
||||
matches = append(matches, staticFSMatch{fileSystem: config[route], name: name})
|
||||
}
|
||||
if ws.staticFSExclusive[candidate] {
|
||||
break
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func (ws *WebServer) findMatchedPathSorted(config []*staticType, requestPath string) string {
|
||||
@ -100,6 +198,11 @@ func (ws *WebServer) findMatchedPathSorted(config []*staticType, requestPath str
|
||||
}
|
||||
|
||||
func (ws *WebServer) processStatic(requestPath string, request *Request, response *Response, logger *log.Logger) bool {
|
||||
for _, match := range ws.getStaticFSMatches(requestPath, request.Host) {
|
||||
if ws.processStaticFS(match, request, response) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
filePath := ws.getStaticFilePath(requestPath, request.Host)
|
||||
if filePath == "" {
|
||||
return false
|
||||
@ -159,6 +262,93 @@ func (ws *WebServer) processStatic(requestPath string, request *Request, respons
|
||||
return false
|
||||
}
|
||||
|
||||
_, _ = response.WriteFiltered(data)
|
||||
if ws.webDevEnabled {
|
||||
_, _ = response.WriteFiltered(data)
|
||||
} else {
|
||||
_, _ = response.Write(data)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ws *WebServer) processStaticFS(match staticFSMatch, request *Request, response *Response) bool {
|
||||
name := match.name
|
||||
opened, err := match.fileSystem.Open(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() { _ = opened.Close() }()
|
||||
info, err := opened.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if info.IsDir() {
|
||||
_ = opened.Close()
|
||||
indexFiles := ws.Config.IndexFiles
|
||||
if len(indexFiles) == 0 {
|
||||
indexFiles = []string{"index.html", "index.htm"}
|
||||
}
|
||||
found := false
|
||||
for _, indexFile := range indexFiles {
|
||||
candidate := pathpkg.Join(name, indexFile)
|
||||
opened, err = match.fileSystem.Open(candidate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
info, err = opened.Stat()
|
||||
if err == nil && !info.IsDir() {
|
||||
name = candidate
|
||||
found = true
|
||||
break
|
||||
}
|
||||
_ = opened.Close()
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if info.IsDir() {
|
||||
return false
|
||||
}
|
||||
seeker, ok := opened.(interface {
|
||||
Read([]byte) (int, error)
|
||||
Seek(int64, int) (int64, error)
|
||||
})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
response.Header().Del(ws.usedDeviceIdKey)
|
||||
response.Header().Del(ws.usedSessionIdKey)
|
||||
contentType := mime.TypeByExtension(filepath.Ext(name))
|
||||
if contentType != "" {
|
||||
response.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
if ws.webDevEnabled && strings.HasPrefix(contentType, "text/html") {
|
||||
data, readErr := io.ReadAll(seeker)
|
||||
if readErr != nil {
|
||||
return false
|
||||
}
|
||||
response.Header().Set("Last-Modified", info.ModTime().UTC().Format(http.TimeFormat))
|
||||
_, _ = response.WriteFiltered(data)
|
||||
return true
|
||||
}
|
||||
http.ServeContent(&staticResponseWriter{response: response}, request.Request, info.Name(), info.ModTime(), seeker)
|
||||
return true
|
||||
}
|
||||
|
||||
type staticResponseWriter struct {
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (w *staticResponseWriter) Header() http.Header {
|
||||
return w.response.Header().H
|
||||
}
|
||||
|
||||
func (w *staticResponseWriter) WriteHeader(statusCode int) {
|
||||
w.response.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *staticResponseWriter) Write(data []byte) (int, error) {
|
||||
return w.response.Write(data)
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = (*staticResponseWriter)(nil)
|
||||
|
||||
@ -8,6 +8,15 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeStaticFixture(t *testing.T, content string) http.FileSystem {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return http.Dir(dir)
|
||||
}
|
||||
|
||||
func TestStaticService(t *testing.T) {
|
||||
// 创建临时测试目录和文件
|
||||
tempDir, _ := os.MkdirTemp("", "static_test")
|
||||
@ -77,3 +86,62 @@ func TestHostStaticService(t *testing.T) {
|
||||
t.Errorf("Expected 404 for mismatched host, got %d", w2.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFSHostCandidatesAndRange(t *testing.T) {
|
||||
ws := NewWebServer()
|
||||
ws.ReplaceStaticFS("*", map[string]http.FileSystem{"/": writeStaticFixture(t, "default")})
|
||||
ws.ReplaceStaticFS(":8081", map[string]http.FileSystem{"/": writeStaticFixture(t, "port")})
|
||||
ws.ReplaceStaticFS("aaa.com", map[string]http.FileSystem{"/": writeStaticFixture(t, "host")})
|
||||
ws.ReplaceStaticFS("aaa.com:8081", map[string]http.FileSystem{"/": writeStaticFixture(t, "exact")})
|
||||
handler := &RouteHandler{ws: ws}
|
||||
|
||||
for _, test := range []struct {
|
||||
host string
|
||||
want string
|
||||
}{
|
||||
{"aaa.com:8081", "exact"},
|
||||
{"aaa.com:9090", "host"},
|
||||
{"other.com:8081", "port"},
|
||||
{"other.com:9090", "default"},
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Host = test.host
|
||||
out := httptest.NewRecorder()
|
||||
handler.ServeHTTP(out, req)
|
||||
if out.Code != http.StatusOK || out.Body.String() != test.want {
|
||||
t.Fatalf("host %s: got %d %q, want 200 %q", test.host, out.Code, out.Body.String(), test.want)
|
||||
}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Host = "aaa.com:8081"
|
||||
req.Header.Set("Range", "bytes=1-2")
|
||||
out := httptest.NewRecorder()
|
||||
handler.ServeHTTP(out, req)
|
||||
if out.Code != http.StatusPartialContent || out.Body.String() != "xa" {
|
||||
t.Fatalf("range response = %d %q", out.Code, out.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFSExclusiveStopsFileFallback(t *testing.T) {
|
||||
ws := NewWebServer()
|
||||
ws.ReplaceStaticFS("*", map[string]http.FileSystem{"/": writeStaticFixture(t, "default")})
|
||||
ws.ReplaceStaticFSExclusive("isolated.example", map[string]http.FileSystem{"/": writeStaticFixture(t, "isolated")})
|
||||
handler := &RouteHandler{ws: ws}
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/missing.txt", nil)
|
||||
request.Host = "isolated.example"
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusNotFound {
|
||||
t.Fatalf("exclusive host missing file returned %d, want 404", response.Code)
|
||||
}
|
||||
|
||||
request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
request.Host = "unknown.example"
|
||||
response = httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || response.Body.String() != "default" {
|
||||
t.Fatalf("unmatched host = %d %q, want default", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user