mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 18:36:37 +08:00
fix(access-logs): 修复概览域名筛选参数 hosts[] 被 Gin 忽略
Axios 默认序列化为 hosts[]=,Gin QueryArray("hosts") 读不到导致筛选失效;
后端兼容 hosts/hosts[],前端改为重复键序列化。
This commit is contained in:
@@ -35,6 +35,7 @@ sidebar: false
|
|||||||
- 修复 Agent 观测缓冲在合并访问日志时忽略 `cache_status` 导致 HIT/MISS 被去重丢弃的问题。
|
- 修复 Agent 观测缓冲在合并访问日志时忽略 `cache_status` 导致 HIT/MISS 被去重丢弃的问题。
|
||||||
- 优化访问日志概览的设备类型与状态码饼图布局,使设备类型与状态码饼图在 1024px(lg)以上屏幕保持双列并排展示,提升页面紧凑度。
|
- 优化访问日志概览的设备类型与状态码饼图布局,使设备类型与状态码饼图在 1024px(lg)以上屏幕保持双列并排展示,提升页面紧凑度。
|
||||||
- 修复访问日志概览在 ClickHouse 查询发生错误时静默吞掉报错的缺陷,引入错误日志输出,方便定位问题。
|
- 修复访问日志概览在 ClickHouse 查询发生错误时静默吞掉报错的缺陷,引入错误日志输出,方便定位问题。
|
||||||
|
- 修复访问日志概览按域名筛选无效的问题:前端 Axios 默认发送 `hosts[]=`,后端 Gin 无法识别,现已兼容 `hosts` / `hosts[]` 两种参数形式。
|
||||||
|
|
||||||
## [v3.4.0] - 2026-07-18
|
## [v3.4.0] - 2026-07-18
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ const apiClient = axios.create({
|
|||||||
withCredentials: apiConfig.withCredentials,
|
withCredentials: apiConfig.withCredentials,
|
||||||
// Fail fast on slash redirect loops between Next dev proxy and Gin legacy routes.
|
// Fail fast on slash redirect loops between Next dev proxy and Gin legacy routes.
|
||||||
maxRedirects: 5,
|
maxRedirects: 5,
|
||||||
|
// Gin QueryArray("hosts") expects hosts=a&hosts=b, not hosts[]=a.
|
||||||
|
paramsSerializer: {
|
||||||
|
indexes: null,
|
||||||
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Serialize query params for Gin-compatible repeated array keys.
|
||||||
|
* Axios default uses hosts[]= which Gin QueryArray("hosts") ignores.
|
||||||
|
*/
|
||||||
|
export function serializeSearchParams(
|
||||||
|
params?: Record<string, unknown>,
|
||||||
|
): string {
|
||||||
|
if (!params) return '';
|
||||||
|
const search = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (value === undefined || value === null || value === '') continue;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
if (item === undefined || item === null || item === '') continue;
|
||||||
|
search.append(key, String(item));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
search.append(key, String(value));
|
||||||
|
}
|
||||||
|
return search.toString();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { serializeSearchParams } from '@/lib/services/core/search-params';
|
||||||
|
|
||||||
|
describe('serializeSearchParams', () => {
|
||||||
|
it('serializes hosts arrays as repeated keys for Gin QueryArray', () => {
|
||||||
|
const query = serializeSearchParams({
|
||||||
|
hours: 168,
|
||||||
|
hosts: ['gist.arctel.de'],
|
||||||
|
});
|
||||||
|
expect(query).toBe('hours=168&hosts=gist.arctel.de');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes multiple hosts without bracket notation', () => {
|
||||||
|
const query = serializeSearchParams({
|
||||||
|
hosts: ['a.example', 'b.example'],
|
||||||
|
});
|
||||||
|
expect(query).toBe('hosts=a.example&hosts=b.example');
|
||||||
|
expect(query).not.toContain('hosts[]');
|
||||||
|
expect(query).not.toContain('hosts%5B%5D');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadQueryStringArrayAcceptsHostsBracketForm(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "axios brackets form",
|
||||||
|
url: "/overview?hours=168&hosts%5B%5D=gist.arctel.de",
|
||||||
|
want: []string{"gist.arctel.de"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repeated hosts keys",
|
||||||
|
url: "/overview?hosts=a.example&hosts=b.example",
|
||||||
|
want: []string{"a.example", "b.example"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single hosts key",
|
||||||
|
url: "/overview?hosts=gist.arctel.de",
|
||||||
|
want: []string{"gist.arctel.de"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
url: "/overview?hours=24",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
req, err := http.NewRequest(http.MethodGet, tc.url, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
c.Request = req
|
||||||
|
|
||||||
|
got := readQueryStringArray(c, "hosts")
|
||||||
|
require.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ func GetAccessLogOverviewHandler(c *gin.Context) {
|
|||||||
result, err := GetAccessLogOverview(c.Request.Context(), AccessLogOverviewQuery{
|
result, err := GetAccessLogOverview(c.Request.Context(), AccessLogOverviewQuery{
|
||||||
NodeID: c.Query("node_id"),
|
NodeID: c.Query("node_id"),
|
||||||
Host: c.Query("host"),
|
Host: c.Query("host"),
|
||||||
Hosts: c.QueryArray("hosts"),
|
Hosts: readQueryStringArray(c, "hosts"),
|
||||||
Hours: readQueryInt(c, "hours"),
|
Hours: readQueryInt(c, "hours"),
|
||||||
})
|
})
|
||||||
if apiutil.AbortBadRequestOnError(c, err) {
|
if apiutil.AbortBadRequestOnError(c, err) {
|
||||||
@@ -279,3 +279,15 @@ func readQueryInt(c *gin.Context, key string) int {
|
|||||||
value, _ := strconv.Atoi(c.DefaultQuery(key, "0"))
|
value, _ := strconv.Atoi(c.DefaultQuery(key, "0"))
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readQueryStringArray reads repeated query values for key, and also accepts
|
||||||
|
// the Axios/jQuery bracket form key[] / key%5B%5D which Gin does not map to key.
|
||||||
|
func readQueryStringArray(c *gin.Context, key string) []string {
|
||||||
|
if values := c.QueryArray(key); len(values) > 0 {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
if values := c.QueryArray(key + "[]"); len(values) > 0 {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user