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}`);

Reply via email to