Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
efc9e1d6
Commit
efc9e1d6
authored
Mar 03, 2026
by
zqq61
Browse files
fix(frontend): prefer upstream payload for generic ops error body
parent
a11ac188
Changes
3
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue
View file @
efc9e1d6
...
@@ -167,6 +167,7 @@ import Icon from '@/components/icons/Icon.vue'
...
@@ -167,6 +167,7 @@ import Icon from '@/components/icons/Icon.vue'
import
{
useAppStore
}
from
'
@/stores
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
opsAPI
,
type
OpsErrorDetail
}
from
'
@/api/admin/ops
'
import
{
opsAPI
,
type
OpsErrorDetail
}
from
'
@/api/admin/ops
'
import
{
formatDateTime
}
from
'
@/utils/format
'
import
{
formatDateTime
}
from
'
@/utils/format
'
import
{
resolvePrimaryResponseBody
,
resolveUpstreamPayload
}
from
'
../utils/errorDetailResponse
'
interface
Props
{
interface
Props
{
show
:
boolean
show
:
boolean
...
@@ -192,11 +193,7 @@ const showUpstreamList = computed(() => props.errorType === 'request')
...
@@ -192,11 +193,7 @@ const showUpstreamList = computed(() => props.errorType === 'request')
const
requestId
=
computed
(()
=>
detail
.
value
?.
request_id
||
detail
.
value
?.
client_request_id
||
''
)
const
requestId
=
computed
(()
=>
detail
.
value
?.
request_id
||
detail
.
value
?.
client_request_id
||
''
)
const
primaryResponseBody
=
computed
(()
=>
{
const
primaryResponseBody
=
computed
(()
=>
{
if
(
!
detail
.
value
)
return
''
return
resolvePrimaryResponseBody
(
detail
.
value
,
props
.
errorType
)
if
(
props
.
errorType
===
'
upstream
'
)
{
return
detail
.
value
.
upstream_error_detail
||
detail
.
value
.
upstream_errors
||
detail
.
value
.
upstream_error_message
||
detail
.
value
.
error_body
||
''
}
return
detail
.
value
.
error_body
||
''
})
})
...
@@ -224,7 +221,9 @@ const correlatedUpstreamErrors = computed<OpsErrorDetail[]>(() => correlatedUpst
...
@@ -224,7 +221,9 @@ const correlatedUpstreamErrors = computed<OpsErrorDetail[]>(() => correlatedUpst
const
expandedUpstreamDetailIds
=
ref
(
new
Set
<
number
>
())
const
expandedUpstreamDetailIds
=
ref
(
new
Set
<
number
>
())
function
getUpstreamResponsePreview
(
ev
:
OpsErrorDetail
):
string
{
function
getUpstreamResponsePreview
(
ev
:
OpsErrorDetail
):
string
{
return
String
(
ev
.
upstream_error_detail
||
ev
.
error_body
||
ev
.
upstream_error_message
||
''
).
trim
()
const
upstreamPayload
=
resolveUpstreamPayload
(
ev
)
if
(
upstreamPayload
)
return
upstreamPayload
return
String
(
ev
.
error_body
||
''
).
trim
()
}
}
function
toggleUpstreamDetail
(
id
:
number
)
{
function
toggleUpstreamDetail
(
id
:
number
)
{
...
...
frontend/src/views/admin/ops/utils/__tests__/errorDetailResponse.spec.ts
0 → 100644
View file @
efc9e1d6
import
{
describe
,
expect
,
it
}
from
'
vitest
'
import
type
{
OpsErrorDetail
}
from
'
@/api/admin/ops
'
import
{
resolvePrimaryResponseBody
,
resolveUpstreamPayload
}
from
'
../errorDetailResponse
'
function
makeDetail
(
overrides
:
Partial
<
OpsErrorDetail
>
):
OpsErrorDetail
{
return
{
id
:
1
,
created_at
:
'
2026-01-01T00:00:00Z
'
,
phase
:
'
request
'
,
type
:
'
api_error
'
,
error_owner
:
'
platform
'
,
error_source
:
'
gateway
'
,
severity
:
'
P2
'
,
status_code
:
502
,
platform
:
'
openai
'
,
model
:
'
gpt-4o-mini
'
,
is_retryable
:
true
,
retry_count
:
0
,
resolved
:
false
,
client_request_id
:
'
crid-1
'
,
request_id
:
'
rid-1
'
,
message
:
'
Upstream request failed
'
,
user_email
:
'
user@example.com
'
,
account_name
:
'
acc
'
,
group_name
:
'
group
'
,
error_body
:
''
,
user_agent
:
''
,
request_body
:
''
,
request_body_truncated
:
false
,
is_business_limited
:
false
,
...
overrides
}
}
describe
(
'
errorDetailResponse
'
,
()
=>
{
it
(
'
prefers upstream payload for request modal when error_body is generic gateway wrapper
'
,
()
=>
{
const
detail
=
makeDetail
({
error_body
:
JSON
.
stringify
({
type
:
'
error
'
,
error
:
{
type
:
'
upstream_error
'
,
message
:
'
Upstream request failed
'
}
}),
upstream_error_detail
:
'
{"provider_message":"real upstream detail"}
'
})
expect
(
resolvePrimaryResponseBody
(
detail
,
'
request
'
)).
toBe
(
'
{"provider_message":"real upstream detail"}
'
)
})
it
(
'
keeps error_body for request modal when body is not generic wrapper
'
,
()
=>
{
const
detail
=
makeDetail
({
error_body
:
JSON
.
stringify
({
type
:
'
error
'
,
error
:
{
type
:
'
upstream_error
'
,
message
:
'
Upstream authentication failed, please contact administrator
'
}
}),
upstream_error_detail
:
'
{"provider_message":"real upstream detail"}
'
})
expect
(
resolvePrimaryResponseBody
(
detail
,
'
request
'
)).
toBe
(
detail
.
error_body
)
})
it
(
'
uses upstream payload first in upstream modal
'
,
()
=>
{
const
detail
=
makeDetail
({
phase
:
'
upstream
'
,
upstream_error_message
:
'
provider 503 overloaded
'
,
error_body
:
'
{"type":"error","error":{"type":"upstream_error","message":"Upstream request failed"}}
'
})
expect
(
resolvePrimaryResponseBody
(
detail
,
'
upstream
'
)).
toBe
(
'
provider 503 overloaded
'
)
})
it
(
'
falls back to upstream payload when request error_body is empty
'
,
()
=>
{
const
detail
=
makeDetail
({
error_body
:
''
,
upstream_error_message
:
'
dial tcp timeout
'
})
expect
(
resolvePrimaryResponseBody
(
detail
,
'
request
'
)).
toBe
(
'
dial tcp timeout
'
)
})
it
(
'
resolves upstream payload by detail -> events -> message priority
'
,
()
=>
{
expect
(
resolveUpstreamPayload
(
makeDetail
({
upstream_error_detail
:
'
detail payload
'
,
upstream_errors
:
'
[{"message":"event payload"}]
'
,
upstream_error_message
:
'
message payload
'
}))).
toBe
(
'
detail payload
'
)
expect
(
resolveUpstreamPayload
(
makeDetail
({
upstream_error_detail
:
''
,
upstream_errors
:
'
[{"message":"event payload"}]
'
,
upstream_error_message
:
'
message payload
'
}))).
toBe
(
'
[{"message":"event payload"}]
'
)
expect
(
resolveUpstreamPayload
(
makeDetail
({
upstream_error_detail
:
''
,
upstream_errors
:
''
,
upstream_error_message
:
'
message payload
'
}))).
toBe
(
'
message payload
'
)
})
it
(
'
treats empty JSON placeholders in upstream payload as empty
'
,
()
=>
{
expect
(
resolveUpstreamPayload
(
makeDetail
({
upstream_error_detail
:
''
,
upstream_errors
:
'
[]
'
,
upstream_error_message
:
''
}))).
toBe
(
''
)
expect
(
resolveUpstreamPayload
(
makeDetail
({
upstream_error_detail
:
''
,
upstream_errors
:
'
{}
'
,
upstream_error_message
:
''
}))).
toBe
(
''
)
expect
(
resolveUpstreamPayload
(
makeDetail
({
upstream_error_detail
:
''
,
upstream_errors
:
'
null
'
,
upstream_error_message
:
''
}))).
toBe
(
''
)
})
it
(
'
skips placeholder candidates and falls back to the next upstream field
'
,
()
=>
{
expect
(
resolveUpstreamPayload
(
makeDetail
({
upstream_error_detail
:
''
,
upstream_errors
:
'
[]
'
,
upstream_error_message
:
'
fallback message
'
}))).
toBe
(
'
fallback message
'
)
expect
(
resolveUpstreamPayload
(
makeDetail
({
upstream_error_detail
:
'
null
'
,
upstream_errors
:
''
,
upstream_error_message
:
'
fallback message
'
}))).
toBe
(
'
fallback message
'
)
})
})
frontend/src/views/admin/ops/utils/errorDetailResponse.ts
0 → 100644
View file @
efc9e1d6
import
type
{
OpsErrorDetail
}
from
'
@/api/admin/ops
'
const
GENERIC_UPSTREAM_MESSAGES
=
new
Set
([
'
upstream request failed
'
,
'
upstream request failed after retries
'
,
'
upstream gateway error
'
,
'
upstream service temporarily unavailable
'
])
type
ParsedGatewayError
=
{
type
:
string
message
:
string
}
function
parseGatewayErrorBody
(
raw
:
string
):
ParsedGatewayError
|
null
{
const
text
=
String
(
raw
||
''
).
trim
()
if
(
!
text
)
return
null
try
{
const
parsed
=
JSON
.
parse
(
text
)
as
Record
<
string
,
any
>
const
err
=
parsed
?.
error
as
Record
<
string
,
any
>
|
undefined
if
(
!
err
||
typeof
err
!==
'
object
'
)
return
null
const
type
=
typeof
err
.
type
===
'
string
'
?
err
.
type
.
trim
()
:
''
const
message
=
typeof
err
.
message
===
'
string
'
?
err
.
message
.
trim
()
:
''
if
(
!
type
&&
!
message
)
return
null
return
{
type
,
message
}
}
catch
{
return
null
}
}
function
isGenericGatewayUpstreamError
(
raw
:
string
):
boolean
{
const
parsed
=
parseGatewayErrorBody
(
raw
)
if
(
!
parsed
)
return
false
if
(
parsed
.
type
!==
'
upstream_error
'
)
return
false
return
GENERIC_UPSTREAM_MESSAGES
.
has
(
parsed
.
message
.
toLowerCase
())
}
export
function
resolveUpstreamPayload
(
detail
:
Pick
<
OpsErrorDetail
,
'
upstream_error_detail
'
|
'
upstream_errors
'
|
'
upstream_error_message
'
>
|
null
|
undefined
):
string
{
if
(
!
detail
)
return
''
const
candidates
=
[
detail
.
upstream_error_detail
,
detail
.
upstream_errors
,
detail
.
upstream_error_message
]
for
(
const
candidate
of
candidates
)
{
const
payload
=
String
(
candidate
||
''
).
trim
()
if
(
!
payload
)
continue
// Normalize common "empty but present" JSON placeholders.
if
(
payload
===
'
[]
'
||
payload
===
'
{}
'
||
payload
.
toLowerCase
()
===
'
null
'
)
{
continue
}
return
payload
}
return
''
}
export
function
resolvePrimaryResponseBody
(
detail
:
OpsErrorDetail
|
null
,
errorType
?:
'
request
'
|
'
upstream
'
):
string
{
if
(
!
detail
)
return
''
const
upstreamPayload
=
resolveUpstreamPayload
(
detail
)
const
errorBody
=
String
(
detail
.
error_body
||
''
).
trim
()
if
(
errorType
===
'
upstream
'
)
{
return
upstreamPayload
||
errorBody
}
if
(
!
errorBody
)
{
return
upstreamPayload
}
// For request detail modal, keep client-visible body by default.
// But if that body is a generic gateway wrapper, show upstream payload first.
if
(
upstreamPayload
&&
isGenericGatewayUpstreamError
(
errorBody
))
{
return
upstreamPayload
}
return
errorBody
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment