Skip to content

Commit 6a6f886

Browse files
authored
Add timeout 504 handling single worker scan requests
* add timeout 504 handling for single file, single worker scan requests. Update unit tests to validate settings
1 parent 03c6e1b commit 6a6f886

6 files changed

Lines changed: 216 additions & 19 deletions

File tree

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,4 @@ linters:
8383
- cyclop
8484
- godot
8585
- funlen
86+
- lll

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.6.4] - 2026-03-23
11+
### Added
12+
- Add support for single worker scan timeout HTTP response handling (504)
13+
1014
## [1.6.3] - 2026-03-10
1115
### Added
1216
- Add dynamic support for loading env vars (from file) during startup.
@@ -195,3 +199,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
195199
[1.6.1]: https://github.com/scanoss/api.go/compare/v1.6.0...v1.6.1
196200
[1.6.2]: https://github.com/scanoss/api.go/compare/v1.6.1...v1.6.2
197201
[1.6.3]: https://github.com/scanoss/api.go/compare/v1.6.2...v1.6.3
202+
[1.6.4]: https://github.com/scanoss/api.go/compare/v1.6.3...v1.6.4

pkg/service/kb_details.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (s APIService) loadKBDetails() {
111111
}
112112
// Load a random (hopefully non-existent) file match to extract the KB version details
113113
emptyConfig := DefaultScanningServiceConfig(s.config)
114-
result, err := s.scanWfp("file=7c53a2de7dfeaa20d057db98468d6670,2321,path/to/dummy/file.txt", "", emptyConfig, zs)
114+
result, _, err := s.scanWfp("file=7c53a2de7dfeaa20d057db98468d6670,2321,path/to/dummy/file.txt", "", emptyConfig, zs)
115115
if err != nil {
116116
zs.Warnf("Failed to detect KB version from eninge: %v", err)
117117
return

pkg/service/scanning_service.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,14 @@ func (s APIService) writeSbomFile(sbom string, zs *zap.SugaredLogger) (*os.File,
231231
// singleScan runs a scan of the WFP in a single thread.
232232
func (s APIService) singleScan(wfp, sbomFile string, config ScanningServiceConfig, zs *zap.SugaredLogger, w http.ResponseWriter) {
233233
zs.Debugf("Single threaded scan...")
234-
result, err := s.scanWfp(wfp, sbomFile, config, zs)
234+
result, timedOut, err := s.scanWfp(wfp, sbomFile, config, zs)
235235
if err != nil {
236+
if timedOut {
237+
http.Error(w, "ERROR engine scan timed out", http.StatusGatewayTimeout)
238+
} else {
239+
http.Error(w, "ERROR engine scan failed", http.StatusInternalServerError)
240+
}
236241
zs.Errorf("Engine scan failed: %v", err)
237-
http.Error(w, "ERROR engine scan failed", http.StatusInternalServerError)
238242
} else {
239243
zs.Debug("Scan completed")
240244
response := strings.TrimSpace(result)
@@ -355,7 +359,7 @@ func (s APIService) workerScan(id string, jobs <-chan string, results chan<- str
355359
zs.Warnf("Nothing in the job request to scan. Ignoring")
356360
results <- ""
357361
} else {
358-
result, err := s.scanWfp(job, sbomFile, config, zs)
362+
result, _, err := s.scanWfp(job, sbomFile, config, zs)
359363
if s.config.App.Trace {
360364
zs.Debugf("scan result (%v): %v, %v", id, result, err)
361365
}
@@ -380,15 +384,15 @@ func (s APIService) workerScan(id string, jobs <-chan string, results chan<- str
380384
}
381385

382386
// scanWfp run the scanoss engine scan of the supplied WFP.
383-
func (s APIService) scanWfp(wfp, sbomFile string, config ScanningServiceConfig, zs *zap.SugaredLogger) (string, error) {
387+
func (s APIService) scanWfp(wfp, sbomFile string, config ScanningServiceConfig, zs *zap.SugaredLogger) (string, bool, error) {
384388
if len(wfp) == 0 {
385389
zs.Warnf("Nothing in the job request to scan. Ignoring")
386-
return "", fmt.Errorf("no wfp supplied to scan. ignoring")
390+
return "", false, fmt.Errorf("no wfp supplied to scan. ignoring")
387391
}
388392
tempFile, err := os.CreateTemp(s.config.Scanning.WfpLoc, "finger*.wfp")
389393
if err != nil {
390394
zs.Errorf("Failed to create temporary file: %v", err)
391-
return "", fmt.Errorf("failed to create temporary WFP file")
395+
return "", false, fmt.Errorf("failed to create temporary WFP file")
392396
}
393397
if s.config.Scanning.TmpFileDelete {
394398
defer removeFile(tempFile, zs)
@@ -398,7 +402,7 @@ func (s APIService) scanWfp(wfp, sbomFile string, config ScanningServiceConfig,
398402
if err != nil {
399403
closeFile(tempFile, zs)
400404
zs.Errorf("Failed to write WFP to temporary file: %v", err)
401-
return "", fmt.Errorf("failed to write to temporary WFP file")
405+
return "", false, fmt.Errorf("failed to write to temporary WFP file")
402406
}
403407
closeFile(tempFile, zs)
404408
// Build command arguments
@@ -448,21 +452,23 @@ func (s APIService) scanWfp(wfp, sbomFile string, config ScanningServiceConfig,
448452
timeoutErr := fmt.Errorf("scan command timed out after %v seconds", s.config.Scanning.ScanTimeout)
449453
ctx, cancel := context.WithTimeoutCause(context.Background(), time.Duration(s.config.Scanning.ScanTimeout)*time.Second, timeoutErr) // put a timeout on the scan execution
450454
defer cancel()
455+
timeoutEncountered := false
451456
//nolint:gosec
452457
output, err := exec.CommandContext(ctx, s.config.Scanning.ScanBinary, args...).Output()
453458
if err != nil {
454459
if cause := context.Cause(ctx); cause != nil {
455460
zs.Errorf("Scan command (%v) timed out: %v", s.config.Scanning.ScanBinary, cause)
461+
timeoutEncountered = true
456462
} else {
457463
zs.Errorf("Scan command (%v %v) failed: %v", s.config.Scanning.ScanBinary, args, err)
458464
}
459465
zs.Errorf("Command output: %s", bytes.TrimSpace(output))
460466
if s.config.Scanning.KeepFailedWfps {
461467
s.copyWfpTempFile(tempFile.Name(), zs)
462468
}
463-
return "", fmt.Errorf("failed to scan WFP: %v", err)
469+
return "", timeoutEncountered, fmt.Errorf("failed to scan WFP: %v", err)
464470
}
465-
return string(output), err
471+
return string(output), timeoutEncountered, err
466472
}
467473

468474
// TestEngine tests if the SCANOSS engine is accessible and running.

pkg/service/scanning_service_test.go

Lines changed: 161 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,21 @@ func TestScanDirectSingle(t *testing.T) {
112112
myConfig := setupConfig(t)
113113
myConfig.App.Trace = true
114114
myConfig.Scanning.ScanDebug = true
115+
myConfig.Scanning.MatchConfigAllowed = false
116+
myConfig.Scanning.RankingAllowed = false
115117
apiService := NewAPIService(myConfig)
116118

117119
tests := []struct {
118-
name string
119-
fieldName string
120-
file string
121-
binary string
122-
telemetry bool
123-
scanType string
124-
assets string
125-
want int
120+
name string
121+
fieldName string
122+
file string
123+
binary string
124+
telemetry bool
125+
scanType string
126+
assets string
127+
scanSettingsB64 string
128+
settingsAllowed bool
129+
want int
126130
}{
127131
{
128132
name: "Scanning - wrong name",
@@ -186,6 +190,77 @@ func TestScanDirectSingle(t *testing.T) {
186190
assets: "pkg:github/org/repo",
187191
want: http.StatusOK,
188192
},
193+
{
194+
name: "Scanning - Settings - invalid base64",
195+
binary: "../../test-support/scanoss.sh",
196+
fieldName: "filename",
197+
file: "./tests/fingers.wfp",
198+
settingsAllowed: true,
199+
scanSettingsB64: "invalid-base64!!!",
200+
want: http.StatusBadRequest,
201+
},
202+
{
203+
name: "Scanning - Settings - invalid json",
204+
binary: "../../test-support/scanoss.sh",
205+
fieldName: "filename",
206+
file: "./tests/fingers.wfp",
207+
// Base64 decoded JSON:
208+
// {
209+
// "field": "something,
210+
// "array": [
211+
// }
212+
scanSettingsB64: "ewoiZmllbGQiOiAic29tZXRoaW5nLAogImFycmF5IjogWwp9",
213+
settingsAllowed: true,
214+
want: http.StatusBadRequest,
215+
},
216+
{
217+
name: "Scanning - Settings - not allowed",
218+
binary: "../../test-support/scanoss.sh",
219+
fieldName: "filename",
220+
file: "./tests/fingers.wfp",
221+
// Base64 decoded JSON:
222+
// {
223+
// "min_snippet_hits": 5,
224+
// "min_snippet_lines": 10
225+
// }
226+
scanSettingsB64: "eyJtaW5fc25pcHBldF9oaXRzIjo1LCJtaW5fc25pcHBldF9saW5lcyI6MTB9",
227+
settingsAllowed: false,
228+
want: http.StatusBadRequest,
229+
},
230+
{
231+
name: "Scanning - Settings - success 1",
232+
binary: "../../test-support/scanoss.sh",
233+
fieldName: "filename",
234+
file: "./tests/fingers.wfp",
235+
// Base64 decoded JSON:
236+
// {
237+
// "ranking_enabled": true,
238+
// "ranking_threshold": 85,
239+
// "min_snippet_hits": 3,
240+
// "min_snippet_lines": 8,
241+
// "honour_file_exts": false
242+
// }
243+
scanSettingsB64: "eyJyYW5raW5nX2VuYWJsZWQiOnRydWUsInJhbmtpbmdfdGhyZXNob2xkIjo4NSwibWluX3NuaXBwZXRfaGl0cyI6MywibWluX3NuaXBwZXRfbGluZXMiOjgsImhvbm91cl9maWxlX2V4dHMiOmZhbHNlfQ==",
244+
settingsAllowed: true,
245+
want: http.StatusOK,
246+
},
247+
{
248+
name: "Scanning - Settings - success 2",
249+
binary: "../../test-support/scanoss.sh",
250+
fieldName: "filename",
251+
file: "./tests/fingers.wfp",
252+
// Base64 decoded JSON:
253+
// {
254+
// "ranking_enabled": true,
255+
// "ranking_threshold": -1,
256+
// "min_snippet_hits": 3,
257+
// "min_snippet_lines": 8,
258+
// "honour_file_exts": true
259+
// }
260+
scanSettingsB64: "ewogICJyYW5raW5nX2VuYWJsZWQiOiB0cnVlLAogICJyYW5raW5nX3RocmVzaG9sZCI6IC0xLAogICJtaW5fc25pcHBldF9oaXRzIjogMywKICAibWluX3NuaXBwZXRfbGluZXMiOiA4LAogICJob25vdXJfZmlsZV9leHRzIjogdHJ1ZQp9",
261+
settingsAllowed: true,
262+
want: http.StatusOK,
263+
},
189264
}
190265
for _, test := range tests {
191266
t.Run(test.name, func(t *testing.T) {
@@ -196,6 +271,8 @@ func TestScanDirectSingle(t *testing.T) {
196271
myConfig.App.Trace = true
197272
}
198273
}
274+
myConfig.Scanning.MatchConfigAllowed = test.settingsAllowed
275+
myConfig.Scanning.RankingEnabled = test.settingsAllowed
199276
myConfig.Scanning.ScanBinary = test.binary
200277
myConfig.Telemetry.Enabled = test.telemetry
201278
filePath := test.file
@@ -225,9 +302,11 @@ func TestScanDirectSingle(t *testing.T) {
225302
}
226303
}
227304
_ = mw.Close() // close the writer before making the request
228-
229305
req := httptest.NewRequest(http.MethodPost, "http://localhost/scan/direct", postBody)
230306
w := httptest.NewRecorder()
307+
if len(test.scanSettingsB64) > 0 {
308+
req.Header.Set("Scanoss-Settings", test.scanSettingsB64)
309+
}
231310
req.Header.Add("Content-Type", mw.FormDataContentType())
232311
apiService.ScanDirect(w, req)
233312
resp := w.Result()
@@ -449,3 +528,76 @@ func TestScanDirectSingleHPSM(t *testing.T) {
449528
})
450529
}
451530
}
531+
532+
func TestScanDirectSingleSlow(t *testing.T) {
533+
err := zlog.NewSugaredDevLogger()
534+
if err != nil {
535+
t.Fatalf("an error '%s' was not expected when opening a sugared logger", err)
536+
}
537+
defer zlog.SyncZap()
538+
myConfig := setupConfig(t)
539+
myConfig.App.Trace = true
540+
myConfig.Scanning.ScanDebug = true
541+
myConfig.Scanning.ScanTimeout = 5
542+
apiService := NewAPIService(myConfig)
543+
544+
tests := []struct {
545+
name string
546+
fieldName string
547+
file string
548+
binary string
549+
scanType string
550+
assets string
551+
want int
552+
}{
553+
{
554+
name: "Scanning - success 1",
555+
binary: "../../test-support/scanoss.sh",
556+
fieldName: "file",
557+
file: "./tests/fingers.wfp",
558+
want: http.StatusOK,
559+
},
560+
{
561+
name: "Scanning - Slow fail",
562+
binary: "../../test-support/scanoss-slow.sh",
563+
fieldName: "filename",
564+
file: "./tests/fingers-hpsm.wfp",
565+
want: http.StatusGatewayTimeout,
566+
},
567+
}
568+
for _, test := range tests {
569+
t.Run(test.name, func(t *testing.T) {
570+
myConfig.Scanning.ScanBinary = test.binary
571+
filePath := test.file
572+
fieldName := test.fieldName
573+
postBody := new(bytes.Buffer)
574+
mw := multipart.NewWriter(postBody)
575+
file, err := os.Open(filePath)
576+
if err != nil {
577+
t.Fatal(err)
578+
}
579+
writer, err := mw.CreateFormFile(fieldName, filePath)
580+
if err != nil {
581+
t.Fatal(err)
582+
}
583+
if _, err = io.Copy(writer, file); err != nil {
584+
t.Fatal(err)
585+
}
586+
_ = mw.Close() // close the writer before making the request
587+
588+
req := httptest.NewRequest(http.MethodPost, "http://localhost/scan/direct", postBody)
589+
w := httptest.NewRecorder()
590+
req.Header.Add("Content-Type", mw.FormDataContentType())
591+
apiService.ScanDirect(w, req)
592+
resp := w.Result()
593+
body, err := io.ReadAll(resp.Body)
594+
if err != nil {
595+
t.Fatalf("an error was not expected when reading from request: %v", err)
596+
}
597+
assert.Equal(t, test.want, resp.StatusCode)
598+
fmt.Println("Status: ", resp.StatusCode)
599+
fmt.Println("Type: ", resp.Header.Get("Content-Type"))
600+
fmt.Println("Body: ", string(body))
601+
})
602+
}
603+
}

test-support/scanoss-slow.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
###
3+
# SPDX-License-Identifier: GPL-2.0-or-later
4+
#
5+
# Copyright (C) 2018-2023 SCANOSS.COM
6+
#
7+
# This program is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU General Public License as published by
9+
# the Free Software Foundation, either version 2 of the License, or
10+
# (at your option) any later version.
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
###
18+
d=$(dirname "$0")
19+
# Simulate getting file contents
20+
if [ "$1" == "-h" ] || [ "$2" == "-h" ] || [ "$1" == "-help" ] || [ "$2" == "-help" ] ; then
21+
echo "SCANOSS slow engine simulator help"
22+
echo " command options..."
23+
exit 0
24+
fi
25+
export DELAY=10
26+
echo "Info: Running slow simulation delay: $DELAY" >&2
27+
"$d"/scanoss.sh "$@"
28+
EXIT_CODE=$?
29+
# Only sleep if the command finished successfully
30+
if [ $EXIT_CODE -eq 0 ] ; then
31+
sleep $DELAY
32+
fi
33+
exit $EXIT_CODE

0 commit comments

Comments
 (0)