fix: fix hono.file response duplicated content-type header
This commit is contained in:
@@ -56,7 +56,7 @@ fn main() {
|
|||||||
|
|
||||||
// 中间件
|
// 中间件
|
||||||
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
||||||
println('[LOG] ${c.url}')
|
println('[LOG] ${c.path}')
|
||||||
return next(mut c)
|
return next(mut c)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ return c.json('{"error": "Not Found"}')
|
|||||||
```v
|
```v
|
||||||
// 日志中间件
|
// 日志中间件
|
||||||
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
||||||
println('[${time.now()}] ${c.req.method} ${c.url}')
|
println('[${time.now()}] ${c.req.method} ${c.path}')
|
||||||
return next(mut c)
|
return next(mut c)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ fn main() {
|
|||||||
|
|
||||||
// 日志中间件
|
// 日志中间件
|
||||||
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
||||||
println('[LOG] ${c.req.method} ${c.url}')
|
println('[LOG] ${c.req.method} ${c.path}')
|
||||||
return next(mut c)
|
return next(mut c)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 认证中间件
|
// 认证中间件
|
||||||
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
||||||
println("url path: ${c.url}")
|
println("url path: ${c.path}")
|
||||||
token := c.query['token']
|
token := c.query['token']
|
||||||
if c.url != "/" && token == '' {
|
if c.path != "/" && token == '' {
|
||||||
c.status(401)
|
c.status(401)
|
||||||
return c.json('{"error": "Unauthorized", "message": "Token required"}')
|
return c.json('{"error": "Unauthorized", "message": "Token required"}')
|
||||||
}
|
}
|
||||||
@@ -197,6 +197,19 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
return c.text('Hello, World!')
|
return c.text('Hello, World!')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Path 信息展示路由
|
||||||
|
app.get('/path-info', fn (mut c hono.Context) http.Response {
|
||||||
|
// 构建JSON响应
|
||||||
|
mut response := '{'
|
||||||
|
response += '"path_info": {'
|
||||||
|
response += '"path": "${c.path}",'
|
||||||
|
response += '"url": "${c.url}"'
|
||||||
|
response += '}'
|
||||||
|
response += '}'
|
||||||
|
|
||||||
|
return c.json(response)
|
||||||
|
})
|
||||||
|
|
||||||
// ===== 5. API 路由 =====
|
// ===== 5. API 路由 =====
|
||||||
|
|
||||||
// 健康检查
|
// 健康检查
|
||||||
@@ -251,6 +264,21 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
return c.json('{"user_id": "${user_id}", "post_id": "${post_id}", "title": "Sample Post"}')
|
return c.json('{"user_id": "${user_id}", "post_id": "${post_id}", "title": "Sample Post"}')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 路径分析路由
|
||||||
|
app.get('/analyze/*path', fn (mut c hono.Context) http.Response {
|
||||||
|
requested_path := c.params['path']
|
||||||
|
|
||||||
|
// 构建JSON响应
|
||||||
|
mut response := '{'
|
||||||
|
response += '"path_analysis": {'
|
||||||
|
response += '"original_path": "${c.path}",'
|
||||||
|
response += '"requested_path": "${requested_path}"'
|
||||||
|
response += '}'
|
||||||
|
response += '}'
|
||||||
|
|
||||||
|
return c.json(response)
|
||||||
|
})
|
||||||
|
|
||||||
// ===== 7. HTTP 方法示例 =====
|
// ===== 7. HTTP 方法示例 =====
|
||||||
|
|
||||||
// POST 请求
|
// POST 请求
|
||||||
@@ -323,7 +351,7 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
|
|
||||||
// DELETE 请求示例
|
// DELETE 请求示例
|
||||||
app.delete('/api/users/:id', fn (mut c hono.Context) http.Response {
|
app.delete('/api/users/:id', fn (mut c hono.Context) http.Response {
|
||||||
user_id := c.params['id']
|
_ := c.params['id'] // 忽略用户ID
|
||||||
c.status(204)
|
c.status(204)
|
||||||
return c.text('')
|
return c.text('')
|
||||||
})
|
})
|
||||||
@@ -458,6 +486,8 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
println(' - GET /api/status')
|
println(' - GET /api/status')
|
||||||
println(' - GET /api/static-info')
|
println(' - GET /api/static-info')
|
||||||
println(' - GET /hello')
|
println(' - GET /hello')
|
||||||
|
println(' - GET /path-info')
|
||||||
|
println(' - GET /analyze/*path')
|
||||||
println('')
|
println('')
|
||||||
println('📄 文件服务端点:')
|
println('📄 文件服务端点:')
|
||||||
println(' - GET /file/:filename - 基本文件服务')
|
println(' - GET /file/:filename - 基本文件服务')
|
||||||
|
|||||||
+24
-3
@@ -11,6 +11,7 @@ pub:
|
|||||||
params map[string]string
|
params map[string]string
|
||||||
query map[string]string
|
query map[string]string
|
||||||
url string
|
url string
|
||||||
|
path string // 当前请求的路径
|
||||||
pub mut:
|
pub mut:
|
||||||
status_code int = 200
|
status_code int = 200
|
||||||
headers map[string]string
|
headers map[string]string
|
||||||
@@ -30,7 +31,8 @@ pub fn Context.new(req http.Request, params map[string]string, query map[string]
|
|||||||
params: params
|
params: params
|
||||||
query: query
|
query: query
|
||||||
body: body
|
body: body
|
||||||
url: url.path
|
url: url.str() // 设置为完整的URL字符串
|
||||||
|
path: url.path // 设置 path 属性
|
||||||
headers: map[string]string{}
|
headers: map[string]string{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +147,16 @@ pub fn (mut c Context) file_with_options(file_path string, options FileOptions)
|
|||||||
if_none_match := c.req.header.get_custom('If-None-Match') or { '' }
|
if_none_match := c.req.header.get_custom('If-None-Match') or { '' }
|
||||||
if if_none_match == etag {
|
if if_none_match == etag {
|
||||||
c.status(304)
|
c.status(304)
|
||||||
return c.text('')
|
// 构建响应头
|
||||||
|
mut headers := http.new_header()
|
||||||
|
for key, value in c.headers {
|
||||||
|
headers.add_custom(key, value) or { continue }
|
||||||
|
}
|
||||||
|
return http.Response{
|
||||||
|
status_code: c.status_code
|
||||||
|
header: headers
|
||||||
|
body: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +173,15 @@ pub fn (mut c Context) file_with_options(file_path string, options FileOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 返回文件内容
|
// 返回文件内容
|
||||||
return c.text(file_content)
|
mut headers := http.new_header()
|
||||||
|
for key, value in c.headers {
|
||||||
|
headers.add_custom(key, value) or { continue }
|
||||||
|
}
|
||||||
|
return http.Response{
|
||||||
|
status_code: c.status_code
|
||||||
|
header: headers
|
||||||
|
body: file_content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileOptions struct for configuring file serving
|
// FileOptions struct for configuring file serving
|
||||||
@@ -240,6 +259,8 @@ pub fn (mut c Context) status(code int) {
|
|||||||
c.status_code = code
|
c.status_code = code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 处理器接口,使用 Context
|
// 处理器接口,使用 Context
|
||||||
pub interface IHandler {
|
pub interface IHandler {
|
||||||
path string
|
path string
|
||||||
|
|||||||
+22
-5
@@ -35,12 +35,12 @@ pub fn default_static_options() StaticOptions {
|
|||||||
pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) http.Response) http.Response {
|
pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) http.Response) http.Response {
|
||||||
return fn [options] (mut c Context, next fn (mut Context) http.Response) http.Response {
|
return fn [options] (mut c Context, next fn (mut Context) http.Response) http.Response {
|
||||||
// 检查请求路径是否匹配静态文件路径
|
// 检查请求路径是否匹配静态文件路径
|
||||||
if !c.url.starts_with(options.path) {
|
if !c.path.starts_with(options.path) {
|
||||||
return next(mut c)
|
return next(mut c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取文件路径
|
// 提取文件路径
|
||||||
mut file_path := c.url[options.path.len..]
|
mut file_path := c.path[options.path.len..]
|
||||||
if file_path.starts_with('/') {
|
if file_path.starts_with('/') {
|
||||||
file_path = file_path[1..]
|
file_path = file_path[1..]
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) htt
|
|||||||
|
|
||||||
// 调试信息
|
// 调试信息
|
||||||
println('[DEBUG] Static file request:')
|
println('[DEBUG] Static file request:')
|
||||||
println(' URL: ${c.url}')
|
println(' Path: ${c.path}')
|
||||||
println(' Path prefix: ${options.path}')
|
println(' Path prefix: ${options.path}')
|
||||||
println(' File path: ${file_path}')
|
println(' File path: ${file_path}')
|
||||||
println(' Root: ${options.root}')
|
println(' Root: ${options.root}')
|
||||||
@@ -137,7 +137,16 @@ fn serve_file(mut c Context, file_path string, options StaticOptions) http.Respo
|
|||||||
if_none_match := c.req.header.get_custom('If-None-Match') or { '' }
|
if_none_match := c.req.header.get_custom('If-None-Match') or { '' }
|
||||||
if if_none_match == etag {
|
if if_none_match == etag {
|
||||||
c.status(304)
|
c.status(304)
|
||||||
return c.text('')
|
// 构建响应头
|
||||||
|
mut headers := http.new_header()
|
||||||
|
for key, value in c.headers {
|
||||||
|
headers.add_custom(key, value) or { continue }
|
||||||
|
}
|
||||||
|
return http.Response{
|
||||||
|
status_code: c.status_code
|
||||||
|
header: headers
|
||||||
|
body: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +163,15 @@ fn serve_file(mut c Context, file_path string, options StaticOptions) http.Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 返回文件内容
|
// 返回文件内容
|
||||||
return c.text(file_content)
|
mut headers := http.new_header()
|
||||||
|
for key, value in c.headers {
|
||||||
|
headers.add_custom(key, value) or { continue }
|
||||||
|
}
|
||||||
|
return http.Response{
|
||||||
|
status_code: c.status_code
|
||||||
|
header: headers
|
||||||
|
body: file_content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安全检查:防止路径遍历攻击
|
// 安全检查:防止路径遍历攻击
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
+57
-4
@@ -327,7 +327,7 @@
|
|||||||
<button class="test-button" onclick="testFile('/file/test.txt')">基本文件服务</button>
|
<button class="test-button" onclick="testFile('/file/test.txt')">基本文件服务</button>
|
||||||
<button class="test-button" onclick="testFile('/file-cached/test.txt')">缓存文件服务</button>
|
<button class="test-button" onclick="testFile('/file-cached/test.txt')">缓存文件服务</button>
|
||||||
<button class="test-button" onclick="testFile('/download/downloadable.txt')">文件下载</button>
|
<button class="test-button" onclick="testFile('/download/downloadable.txt')">文件下载</button>
|
||||||
<button class="test-button" onclick="testFile('/image/style.css')">图片文件服务</button>
|
<button class="test-button" onclick="testFile('/image/img1.jpg')">图片文件服务</button>
|
||||||
<button class="test-button" onclick="testFile('/safe-file/test.txt')">安全文件服务</button>
|
<button class="test-button" onclick="testFile('/safe-file/test.txt')">安全文件服务</button>
|
||||||
<div id="file-result" class="result" style="display: none;"></div>
|
<div id="file-result" class="result" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -612,11 +612,64 @@ app.listen(':8080')
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url.toString());
|
const response = await fetch(url.toString());
|
||||||
const data = await response.text();
|
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = Date.now() - startTime;
|
||||||
|
|
||||||
resultDiv.className = 'result';
|
// 获取所有响应头信息用于调试
|
||||||
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n文件大小: ${data.length} 字符\n内容预览:\n${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`;
|
const headers = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否是图片文件 - 优先检查文件扩展名
|
||||||
|
const urlPath = url.pathname.toLowerCase();
|
||||||
|
const isImageByExtension = urlPath.endsWith('.jpg') || urlPath.endsWith('.jpeg') ||
|
||||||
|
urlPath.endsWith('.png') || urlPath.endsWith('.gif') ||
|
||||||
|
urlPath.endsWith('.webp') || urlPath.endsWith('.svg');
|
||||||
|
|
||||||
|
// 也检查 content-type
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
const isImageByType = contentType.startsWith('image/');
|
||||||
|
|
||||||
|
const isImage = isImageByExtension || isImageByType;
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
// 对于图片文件,显示图片预览
|
||||||
|
const blob = await response.blob();
|
||||||
|
const imageUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
resultDiv.className = 'result';
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
✅ 成功 (${responseTime}ms)<br>
|
||||||
|
状态码: ${response.status}<br>
|
||||||
|
请求URL: ${url.toString()}<br>
|
||||||
|
文件类型: ${contentType}<br>
|
||||||
|
文件大小: ${(blob.size / 1024).toFixed(2)} KB<br>
|
||||||
|
检测方式: ${isImageByExtension ? '文件扩展名' : 'Content-Type'}<br>
|
||||||
|
<br>
|
||||||
|
<strong>响应头信息:</strong><br>
|
||||||
|
<pre style="font-size: 0.8em; background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">${JSON.stringify(headers, null, 2)}</pre>
|
||||||
|
<br>
|
||||||
|
<strong>图片预览:</strong><br>
|
||||||
|
<img src="${imageUrl}" style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 10px;" alt="图片预览">
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// 对于非图片文件,显示文本内容
|
||||||
|
const data = await response.text();
|
||||||
|
resultDiv.className = 'result';
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
✅ 成功 (${responseTime}ms)<br>
|
||||||
|
状态码: ${response.status}<br>
|
||||||
|
请求URL: ${url.toString()}<br>
|
||||||
|
文件类型: ${contentType}<br>
|
||||||
|
文件大小: ${data.length} 字符<br>
|
||||||
|
<br>
|
||||||
|
<strong>响应头信息:</strong><br>
|
||||||
|
<pre style="font-size: 0.8em; background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">${JSON.stringify(headers, null, 2)}</pre>
|
||||||
|
<br>
|
||||||
|
<strong>内容预览:</strong><br>
|
||||||
|
<pre style="font-size: 0.9em; background: #f9f9f9; padding: 10px; border-radius: 4px; overflow-x: auto;">${data.substring(0, 200)}${data.length > 200 ? '...' : ''}</pre>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
updateStats(true, responseTime);
|
updateStats(true, responseTime);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Reference in New Issue
Block a user