Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.12.1] 2026-04-06
### Fixed
- Fixed API key header being set incorrectly in file content requests
- Changed decoration API prefix from `/api/v2` to `/v2`
- Fixed an issue reading decoration API response after the context was cancelled

## [0.12.0] 2026-03-10
### Added
- Restore action: Undo a previous decision (include, dismiss, replace) on a completed result, returning it to the pending state
Expand Down Expand Up @@ -264,3 +270,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[0.10.0]: https://github.com/scanoss/scanoss.cc/compare/v0.9.9...v0.10.0
[0.11.0]: https://github.com/scanoss/scanoss.cc/compare/v0.10.0...v0.11.0
[0.12.0]: https://github.com/scanoss/scanoss.cc/compare/v0.11.0...v0.12.0
[0.12.1]: https://github.com/scanoss/scanoss.cc/compare/v0.12.0...v0.12.1
1 change: 1 addition & 0 deletions backend/repository/file_repository_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func (r *FileRepositoryImpl) ReadRemoteFileByMD5(path string, md5 string) (entit
headers := make(map[string]string)
if token != "" {
headers["X-Session"] = token
headers["X-API-Key"] = token
}

options := fetch.Options{
Expand Down
70 changes: 36 additions & 34 deletions backend/service/scanoss_api_service_http_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,11 @@ func (s *ScanossApiServiceHttpImpl) buildURL(endpoint string, params QueryParams
return u.String(), nil
}

func (s *ScanossApiServiceHttpImpl) GetWithParams(endpoint string, params QueryParams) (*http.Response, error) {
func (s *ScanossApiServiceHttpImpl) GetWithParams(ctx context.Context, endpoint string, params QueryParams) (*http.Response, error) {
fullURL, err := s.buildURL(endpoint, params)
if err != nil {
return nil, fmt.Errorf("failed to build URL: %w", err)
}

ctx := s.ctx
if ctx == nil {
ctx = context.Background()
}

ctx, cancel := context.WithCancel(ctx)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
Expand All @@ -120,9 +111,9 @@ func (s *ScanossApiServiceHttpImpl) GetWithParams(endpoint string, params QueryP

resp, err := s.client.Do(req)
if err != nil {
log.Debug().Msgf("Get Request to %s failed: %v", fullURL, err)
return nil, fmt.Errorf("request failed: %w", err)
}

return resp, nil
}

Expand All @@ -132,61 +123,64 @@ func (s *ScanossApiServiceHttpImpl) SearchComponents(request entities.ComponentS
log.Error().Err(err).Msg("Invalid component search request")
return entities.ComponentSearchResponse{}, fmt.Errorf("invalid search request: %w", err)
}

log.Debug().
Str("search", request.Search).
Str("vendor", request.Vendor).
Str("component", request.Component).
Str("package", request.Package).
Int32("limit", request.Limit).
Msg("Searching components via SCANOSS API")

if s.apiKey == "" {
log.Error().Msg("SCANOSS API key not configured")
return entities.ComponentSearchResponse{}, fmt.Errorf("SCANOSS API key not configured")
}

params := QueryParams{
"search": request.Search,
}

if request.Vendor != "" {
params["vendor"] = request.Vendor
}

if request.Component != "" {
params["component"] = request.Component
}

if request.Package != "" {
params["package"] = request.Package
}

if request.Limit > 0 {
params["limit"] = fmt.Sprintf("%d", request.Limit)
}

if request.Offset > 0 {
params["offset"] = fmt.Sprintf("%d", request.Offset)
}

resp, err := s.GetWithParams("/api/v2/components/search", params)
if s.ctx == nil {
log.Warn().Msg("No context provided, using default context")
s.ctx = context.Background()
}
ctx, cancel := context.WithCancel(s.ctx)
defer cancel()
Comment on lines +156 to +161
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n backend/service/scanoss_api_service_http_impl.go | head -50

Repository: scanoss/scanoss.cc

Length of output: 2049


🏁 Script executed:

wc -l backend/service/scanoss_api_service_http_impl.go

Repository: scanoss/scanoss.cc

Length of output: 114


🏁 Script executed:

cat -n backend/service/scanoss_api_service_http_impl.go | sed -n '140,180p'

Repository: scanoss/scanoss.cc

Length of output: 1785


🏁 Script executed:

cat -n backend/service/scanoss_api_service_http_impl.go | sed -n '200,230p'

Repository: scanoss/scanoss.cc

Length of output: 1453


🏁 Script executed:

cat -n backend/service/scanoss_api_service_http_impl.go | sed -n '1,80p'

Repository: scanoss/scanoss.cc

Length of output: 2905


🏁 Script executed:

rg -n "NewScanossApiServiceHttpImpl|ScanossApiServiceHttpImpl" --type go -A 3 -B 1 | head -100

Repository: scanoss/scanoss.cc

Length of output: 4101


🏁 Script executed:

rg -n "SetContext" --type go -A 2 -B 1

Repository: scanoss/scanoss.cc

Length of output: 6672


🏁 Script executed:

rg -n "go func" backend/service/scanoss_api_service_http_impl.go

Repository: scanoss/scanoss.cc

Length of output: 44


🏁 Script executed:

# Check if there's any synchronization (mutex, locks) in the struct or usage
rg -n "sync\.|Mutex|RWMutex|Lock" backend/service/ --type go

Repository: scanoss/scanoss.cc

Length of output: 1125


🏁 Script executed:

cat -n main.go | sed -n '100,130p'

Repository: scanoss/scanoss.cc

Length of output: 1184


🏁 Script executed:

# Check if there's an HTTP server handling concurrent requests
rg -n "http.Server|Server|ListenAndServe|Handler" main.go -A 3 -B 1

Repository: scanoss/scanoss.cc

Length of output: 214


🏁 Script executed:

# Check if Wails framework enables concurrent request handling
cd backend && find . -name "*.go" -type f | head -20 | xargs grep -l "goroutine\|concurrent\|channel" | head -5

Repository: scanoss/scanoss.cc

Length of output: 44


🏁 Script executed:

# Check the app binding to see what methods can be called concurrently from UI
cat -n main.go | sed -n '110,140p'

Repository: scanoss/scanoss.cc

Length of output: 1031


🏁 Script executed:

# Verify if there are any other places where s.ctx is read/modified
rg "s\.ctx" backend/service/scanoss_api_service_http_impl.go -n

Repository: scanoss/scanoss.cc

Length of output: 285


Use a local variable for context fallback instead of mutating the service field.

Lines 156-158 and 209-211 mutate s.ctx during request handling. Since this service instance is reused across requests and exposed via Wails bindings (allowing concurrent invocation), this creates a race condition. Concurrent requests can interfere with each other's context, and the fallback assignment can persist to unrelated calls.

Capture s.ctx once in a local variable before the nil check to ensure each request operates on a stable context value:

Suggested change
-	if s.ctx == nil {
-		log.Warn().Msg("No context provided, using default context")
-		s.ctx = context.Background()
-	}
-	ctx, cancel := context.WithCancel(s.ctx)
+	baseCtx := s.ctx
+	if baseCtx == nil {
+		log.Warn().Msg("No context provided, using default context")
+		baseCtx = context.Background()
+	}
+	ctx, cancel := context.WithCancel(baseCtx)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if s.ctx == nil {
log.Warn().Msg("No context provided, using default context")
s.ctx = context.Background()
}
ctx, cancel := context.WithCancel(s.ctx)
defer cancel()
baseCtx := s.ctx
if baseCtx == nil {
log.Warn().Msg("No context provided, using default context")
baseCtx = context.Background()
}
ctx, cancel := context.WithCancel(baseCtx)
defer cancel()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/service/scanoss_api_service_http_impl.go` around lines 156 - 161, The
code currently mutates s.ctx during request handling (see s.ctx usage and the
context.WithCancel call), which creates a race for concurrent requests; instead,
capture the field into a local variable (e.g., localCtx := s.ctx), if localCtx
== nil assign localCtx = context.Background(), then call
context.WithCancel(localCtx) and use that local context for the request—do this
for both occurrences that assign s.ctx (the block using s.ctx and the similar
block at lines ~209-211) so the service field is never modified during request
handling.

// Send the component search request
resp, err := s.GetWithParams(ctx, "/v2/components/search", params)
if err != nil {
log.Error().Err(err).Msg("Error calling SCANOSS component search API")
return entities.ComponentSearchResponse{}, fmt.Errorf("API call failed: %w", err)
}
defer resp.Body.Close()

body, bodyErr := io.ReadAll(resp.Body)
if bodyErr != nil {
log.Warn().Err(bodyErr).Msg("Failed to read response body")
} else {
log.Debug().Msgf("Response body: '%v'", string(body))
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Error().Int("statusCode", resp.StatusCode).Str("body", string(body)).Msg("API returned non-200 status")
return entities.ComponentSearchResponse{}, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
Comment thread
eeisegn marked this conversation as resolved.
}

var apiResponse entities.ComponentSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
return entities.ComponentSearchResponse{}, fmt.Errorf("failed to decode response: %w", err)
if jsonErr := json.Unmarshal(body, &apiResponse); jsonErr != nil {
log.Error().Err(jsonErr).Msgf("Failed to decode response body: %v", jsonErr)
return entities.ComponentSearchResponse{}, fmt.Errorf("failed to decode response: %w", jsonErr)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

log.Debug().
Int("componentCount", len(apiResponse.Components)).
Msg("Successfully retrieved component search results")
Expand All @@ -212,24 +206,32 @@ func (s *ScanossApiServiceHttpImpl) GetLicensesByPurl(request entities.Component
"purl": request.Purl,
"requirement": request.Requirement,
}

resp, err := s.GetWithParams("/api/v2/licenses/component", params)
if s.ctx == nil {
log.Warn().Msg("No context provided, using default context")
s.ctx = context.Background()
}
ctx, cancel := context.WithCancel(s.ctx)
defer cancel()
resp, err := s.GetWithParams(ctx, "/v2/licenses/component", params)
if err != nil {
log.Error().Err(err).Msg("Error calling SCANOSS component search API")
return entities.GetLicensesByPurlResponse{}, fmt.Errorf("API call failed: %w", err)
}
defer resp.Body.Close()

body, bodyErr := io.ReadAll(resp.Body)
if bodyErr != nil {
log.Warn().Err(bodyErr).Msg("Failed to read response body")
} else {
log.Debug().Msgf("Response body: '%v'", string(body))
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Error().Int("statusCode", resp.StatusCode).Str("body", string(body)).Msg("API returned non-200 status")
return entities.GetLicensesByPurlResponse{}, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}

var apiResponse entities.GetLicensesByPurlResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
return entities.GetLicensesByPurlResponse{}, fmt.Errorf("failed to decode response: %w", err)
if jsonErr := json.Unmarshal(body, &apiResponse); jsonErr != nil {
log.Error().Err(jsonErr).Msgf("Failed to decode response body: %v", jsonErr)
return entities.GetLicensesByPurlResponse{}, fmt.Errorf("failed to decode response: %w", jsonErr)
}

return apiResponse, nil
}
Loading