This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/devlake.git
The following commit(s) were added to refs/heads/main by this push:
new ee0079ab3 fix(server): return 502 instead of passing through upstream
401 (#9231)
ee0079ab3 is described below
commit ee0079ab32d4a80220c9369f899120a98db8db34
Author: Rasmus Rask <[email protected]>
AuthorDate: Sun Oct 18 08:13:31 2026 +0200
fix(server): return 502 instead of passing through upstream 401 (#9231)
* fix(server): return 502 instead of passing through upstream 401
* refactor(config-ui): drop plugin test 401 exemption
---
backend/server/api/router.go | 4 +--
backend/server/api/shared/api_output.go | 24 ++++++++++-----
backend/server/api/shared/api_output_test.go | 44 ++++++++++++++++++++++++++++
config-ui/src/utils/request.ts | 13 +-------
4 files changed, 63 insertions(+), 22 deletions(-)
diff --git a/backend/server/api/router.go b/backend/server/api/router.go
index a6d456246..c517e349f 100644
--- a/backend/server/api/router.go
+++ b/backend/server/api/router.go
@@ -150,12 +150,12 @@ func handlePluginCall(basicRes context.BasicRes,
pluginName string, handler plug
if err != nil {
if output != nil && output.Body != nil {
logruslog.Global.Error(err, "")
- shared.ApiOutputSuccess(c, output.Body,
err.GetType().GetHttpCode())
+ shared.ApiOutputSuccess(c, output.Body,
shared.ClientHttpCode(err.GetType().GetHttpCode()))
} else {
shared.ApiOutputError(c, err)
}
} else if output != nil {
- status := output.Status
+ status := shared.ClientHttpCode(output.Status)
if status < http.StatusContinue {
status = http.StatusOK
}
diff --git a/backend/server/api/shared/api_output.go
b/backend/server/api/shared/api_output.go
index cf724d8a9..dba5a2a82 100644
--- a/backend/server/api/shared/api_output.go
+++ b/backend/server/api/shared/api_output.go
@@ -47,9 +47,9 @@ type ResponsePipelines struct {
// ApiOutputErrorWithCustomCode writes a JSON error message to the HTTP
response body
func ApiOutputErrorWithCustomCode(c *gin.Context, code int, err error) {
if e, ok := err.(errors.Error); ok {
- logruslog.Global.Error(err, "HTTP %d error",
e.GetType().GetHttpCode())
+ logruslog.Global.Error(err, "HTTP %d error",
ClientHttpCode(e.GetType().GetHttpCode()))
messages := e.Messages()
- c.JSON(e.GetType().GetHttpCode(), &ApiBody{
+ c.JSON(ClientHttpCode(e.GetType().GetHttpCode()), &ApiBody{
Success: false,
Message: e.Error(),
Code: code,
@@ -69,9 +69,9 @@ func ApiOutputErrorWithCustomCode(c *gin.Context, code int,
err error) {
// ApiOutputAdvancedErrorWithCustomCode writes a JSON error message to the
HTTP response body
func ApiOutputAdvancedErrorWithCustomCode(c *gin.Context, httpStatusCode,
customBusinessCode int, err error) {
if e, ok := err.(errors.Error); ok {
- logruslog.Global.Error(err, "HTTP %d error",
e.GetType().GetHttpCode())
+ logruslog.Global.Error(err, "HTTP %d error",
ClientHttpCode(e.GetType().GetHttpCode()))
messages := e.Messages()
- c.JSON(e.GetType().GetHttpCode(), &ApiBody{
+ c.JSON(ClientHttpCode(e.GetType().GetHttpCode()), &ApiBody{
Success: false,
Message: e.Error(),
Code: customBusinessCode,
@@ -91,9 +91,9 @@ func ApiOutputAdvancedErrorWithCustomCode(c *gin.Context,
httpStatusCode, custom
// ApiOutputError writes a JSON error message to the HTTP response body
func ApiOutputError(c *gin.Context, err error) {
if e, ok := err.(errors.Error); ok {
- logruslog.Global.Error(err, "HTTP %d error",
e.GetType().GetHttpCode())
+ logruslog.Global.Error(err, "HTTP %d error",
ClientHttpCode(e.GetType().GetHttpCode()))
messages := e.Messages()
- c.JSON(e.GetType().GetHttpCode(), &ApiBody{
+ c.JSON(ClientHttpCode(e.GetType().GetHttpCode()), &ApiBody{
Success: false,
Message: e.Error(),
Causes: messages.Causes(),
@@ -122,10 +122,18 @@ func ApiOutputSuccess(c *gin.Context, body interface{},
status int) {
// ApiOutputAbort writes the HTTP response code header and saves the error
internally, but doesn't push it to the response
func ApiOutputAbort(c *gin.Context, err error) {
if e, ok := err.(errors.Error); ok {
- logruslog.Global.Error(err, "HTTP %d abort-error",
e.GetType().GetHttpCode())
- _ = c.AbortWithError(e.GetType().GetHttpCode(),
errors.Default.New(e.Messages().Format()))
+ logruslog.Global.Error(err, "HTTP %d abort-error",
ClientHttpCode(e.GetType().GetHttpCode()))
+ _ = c.AbortWithError(ClientHttpCode(e.GetType().GetHttpCode()),
errors.Default.New(e.Messages().Format()))
} else {
logruslog.Global.Error(err, "HTTP %d abort-error (native)",
http.StatusInternalServerError)
_ = c.AbortWithError(http.StatusInternalServerError, err)
}
}
+
+// ClientHttpCode maps an upstream 401 to 502, since clients treat 401 as an
expired DevLake session
+func ClientHttpCode(code int) int {
+ if code == http.StatusUnauthorized {
+ return http.StatusBadGateway
+ }
+ return code
+}
diff --git a/backend/server/api/shared/api_output_test.go
b/backend/server/api/shared/api_output_test.go
new file mode 100644
index 000000000..ebaf5730f
--- /dev/null
+++ b/backend/server/api/shared/api_output_test.go
@@ -0,0 +1,44 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package shared
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/apache/devlake/core/errors"
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestApiOutputErrorMapsUpstream401(t *testing.T) {
+ tests := []struct {
+ upstream int
+ want int
+ }{
+ {http.StatusUnauthorized, http.StatusBadGateway},
+ {http.StatusNotFound, http.StatusNotFound},
+ }
+ for _, tt := range tests {
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ ApiOutputError(c,
errors.Default.Wrap(errors.HttpStatus(tt.upstream).New("upstream"), "wrapped"))
+ assert.Equal(t, tt.want, w.Code)
+ }
+}
diff --git a/config-ui/src/utils/request.ts b/config-ui/src/utils/request.ts
index 3badcc0d4..e91739f72 100644
--- a/config-ui/src/utils/request.ts
+++ b/config-ui/src/utils/request.ts
@@ -41,29 +41,18 @@ export type RequestConfig = {
const isLoginRoute = () => window.location.pathname.replace(/\/+$/,
'').endsWith('/login');
-// Plugin connection tests often return 401 when the *remote* credential is
-// wrong. That is not a DevLake session expiry — do not wipe the form.
-const isPluginConnectionTest = (url?: string) =>
- !!url && /\/plugins\/[^/]+(?:\/connections\/[^/]+)?\/test\/?$/.test(url);
-
let redirectingToLogin = false;
instance.interceptors.response.use(
(response) => response,
(error) => {
const status = error.response?.status;
- const requestUrl = error.config?.url as string | undefined;
if (status === 428) {
window.location.replace('/db-migrate');
}
- if (
- status === 401 &&
- !isLoginRoute() &&
- !redirectingToLogin &&
- !isPluginConnectionTest(requestUrl)
- ) {
+ if (status === 401 && !isLoginRoute() && !redirectingToLogin) {
redirectingToLogin = true;
const returnUrl = encodeURIComponent(window.location.pathname +
window.location.search);
window.location.replace(`/login?return_url=${returnUrl}`);