@@ -19,7 +19,9 @@ package api
1919
2020import (
2121 "context"
22+ "encoding/json"
2223 "fmt"
24+ "io"
2325 "net/http"
2426 "net/url"
2527 "strings"
@@ -39,8 +41,13 @@ type JiraTestConnResponse struct {
3941}
4042
4143func testConnection (ctx context.Context , connection models.JiraConn ) (* JiraTestConnResponse , errors.Error ) {
42- // validate
43- if vld != nil {
44+ // OAuth2 is plugin-specific and is not in MultiAuth's oneof, so validate it
45+ // separately. Other auth methods keep the existing StructExcept path.
46+ if connection .IsOAuth2 () {
47+ if err := connection .ValidateConnection (& connection , vld ); err != nil {
48+ return nil , err
49+ }
50+ } else if vld != nil {
4451 e := vld .StructExcept (connection , "BasicAuth" , "AccessToken" )
4552 if e != nil {
4653 return nil , errors .Convert (e )
@@ -74,10 +81,12 @@ func testConnection(ctx context.Context, connection models.JiraConn) (*JiraTestC
7481 return nil , errors .NotFound .New (fmt .Sprintf ("Seems like an invalid Endpoint URL, please try %s" , restUrl .String ()))
7582 }
7683 if res .StatusCode == http .StatusUnauthorized || res .StatusCode == http .StatusForbidden {
77- return nil , errors .HttpStatus (res .StatusCode ).New ("Please check your credential" )
84+ // 400 not 401: Config UI treats HTTP 401 as a DevLake session expiry and
85+ // navigates to /login, wiping the connection form.
86+ return nil , errors .BadInput .New (withJiraHTTPDetail (res , "Please check your credential" ))
7887 }
7988 if res .StatusCode != http .StatusOK {
80- return nil , errors .HttpStatus (res .StatusCode ).New (fmt .Sprintf ("%s unexpected status code: %d" , serverInfoFail , res .StatusCode ))
89+ return nil , errors .HttpStatus (res .StatusCode ).New (withJiraHTTPDetail ( res , fmt .Sprintf ("%s unexpected status code: %d" , serverInfoFail , res .StatusCode ) ))
8190 }
8291
8392 resBody := & models.JiraServerInfo {}
@@ -101,13 +110,16 @@ func testConnection(ctx context.Context, connection models.JiraConn) (*JiraTestC
101110 }
102111 getStatusFail += ": [ " + res .Request .URL .String () + " ]"
103112
104- errMsg := ""
105- if res .StatusCode == http .StatusUnauthorized {
106- return nil , errors .HttpStatus (res .StatusCode ).New ("Please check your credential" )
113+ if res .StatusCode == http .StatusUnauthorized || res .StatusCode == http .StatusForbidden {
114+ return nil , errors .BadInput .New (withJiraHTTPDetail (res , fmt .Sprintf (
115+ "Jira accepted the connection (serverInfo OK) but GET %s returned %d" ,
116+ res .Request .URL .String (),
117+ res .StatusCode ,
118+ )))
107119 }
108120
109121 if res .StatusCode != http .StatusOK {
110- return nil , errors .HttpStatus (res .StatusCode ).New (fmt .Sprintf ("%s Unexpected [%s] status code: %d %s " , getStatusFail , res .Request .URL , res .StatusCode , errMsg ))
122+ return nil , errors .HttpStatus (res .StatusCode ).New (withJiraHTTPDetail ( res , fmt .Sprintf ("%s Unexpected [%s] status code: %d" , getStatusFail , res .Request .URL , res .StatusCode ) ))
111123 }
112124 connection = connection .Sanitize ()
113125 body := JiraTestConnResponse {}
@@ -222,3 +234,62 @@ func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
222234func GetConnection (input * plugin.ApiResourceInput ) (* plugin.ApiResourceOutput , errors.Error ) {
223235 return dsHelper .ConnApi .GetDetail (input )
224236}
237+
238+ func withJiraHTTPDetail (res * http.Response , msg string ) string {
239+ if detail := jiraHTTPErrorDetail (res ); detail != "" {
240+ return msg + ": " + detail
241+ }
242+ return msg
243+ }
244+
245+ // jiraHTTPErrorDetail reads Jira's error payload. Classic REST uses
246+ // errorMessages; the Atlassian OAuth gateway often uses message and/or
247+ // WWW-Authenticate (e.g. insufficient_scope).
248+ func jiraHTTPErrorDetail (res * http.Response ) string {
249+ if res == nil || res .Body == nil {
250+ return ""
251+ }
252+ defer res .Body .Close ()
253+ raw , err := io .ReadAll (io .LimitReader (res .Body , 4096 ))
254+ if err != nil {
255+ return ""
256+ }
257+ if parsed := parseJiraErrorBody (strings .TrimSpace (string (raw ))); parsed != "" {
258+ return parsed
259+ }
260+ if auth := strings .TrimSpace (res .Header .Get ("WWW-Authenticate" )); auth != "" {
261+ return auth
262+ }
263+ text := strings .TrimSpace (string (raw ))
264+ if text == "" || strings .HasPrefix (text , "<" ) {
265+ return ""
266+ }
267+ const maxLen = 500
268+ if len (text ) > maxLen {
269+ return text [:maxLen ] + "..."
270+ }
271+ return text
272+ }
273+
274+ func parseJiraErrorBody (text string ) string {
275+ if text == "" || text [0 ] != '{' {
276+ return ""
277+ }
278+ var payload struct {
279+ ErrorMessages []string `json:"errorMessages"`
280+ Message string `json:"message"`
281+ }
282+ if json .Unmarshal ([]byte (text ), & payload ) != nil {
283+ return ""
284+ }
285+ parts := make ([]string , 0 , len (payload .ErrorMessages )+ 1 )
286+ for _ , m := range payload .ErrorMessages {
287+ if m = strings .TrimSpace (m ); m != "" {
288+ parts = append (parts , m )
289+ }
290+ }
291+ if m := strings .TrimSpace (payload .Message ); m != "" {
292+ parts = append (parts , m )
293+ }
294+ return strings .Join (parts , "; " )
295+ }
0 commit comments