From db695cb88abcccb1bb2cd00199a8bf7568ded9e1 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 24 May 2026 21:13:57 +0100 Subject: [PATCH 1/7] Add validation for returned Content-Type Header within Client.doRequest. Signed-off-by: SolarFactories --- client.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/client.go b/client.go index 7e6a346..0df9200 100644 --- a/client.go +++ b/client.go @@ -16,6 +16,7 @@ import ( "net/http/httputil" "net/url" "os" + "slices" "strconv" "strings" "time" @@ -350,8 +351,14 @@ func (c Client) doRequest(req *http.Request, v interface{}) (a apiResponse, err } if v != nil { + contentType := res.Header.Get("Content-Type") switch vt := v.(type) { case *string: + expectedContentTypes := []string{"text/plain", "application/vnd.cyclonedx+json", "application/vnd.cyclonedx+xml"} + if !slices.Contains(expectedContentTypes, contentType) { + err = fmt.Errorf("Expected %s content-type. Received %s.", strings.Join(expectedContentTypes, ", "), contentType) + return + } if content, readErr := io.ReadAll(res.Body); readErr == nil { *vt = strings.TrimSpace(string(content)) } else { @@ -359,6 +366,10 @@ func (c Client) doRequest(req *http.Request, v interface{}) (a apiResponse, err return } default: + if contentType != "application/json" { + err = fmt.Errorf("Expected application/json content-type. Received %s.", contentType) + return + } err = json.NewDecoder(res.Body).Decode(v) if err != nil { return From 4467ce907d4334f3080c11ddeac98a6efcbcb659 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 24 May 2026 21:14:56 +0100 Subject: [PATCH 2/7] Fixed Accept Header specified in request when exporting BOM to account for XML variant. Signed-off-by: SolarFactories --- bom.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bom.go b/bom.go index 687aaad..c40d725 100644 --- a/bom.go +++ b/bom.go @@ -59,7 +59,12 @@ func (bs BOMService) ExportComponent(ctx context.Context, componentUUID uuid.UUI return } - req.Header.Set("Accept", "application/vnd.cyclonedx+json") + switch format { + case BOMFormatJSON: + req.Header.Set("Accept", "application/vnd.cyclonedx+json") + case BOMFormatXML: + req.Header.Set("Accept", "application/vnd.cyclonedx+xml") + } _, err = bs.client.doRequest(req, &bom) return @@ -79,7 +84,12 @@ func (bs BOMService) ExportProject(ctx context.Context, projectUUID uuid.UUID, f return } - req.Header.Set("Accept", "application/vnd.cyclonedx+json") + switch format { + case BOMFormatJSON: + req.Header.Set("Accept", "application/vnd.cyclonedx+json") + case BOMFormatXML: + req.Header.Set("Accept", "application/vnd.cyclonedx+xml") + } _, err = bs.client.doRequest(req, &bom) return From 0de572b5f34a98400d2ee26183aca0df330a1c80 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 24 May 2026 21:16:07 +0100 Subject: [PATCH 3/7] Tightened Accept Header on UserService.Login from */* to text/plain. Removed needless Accept Header on UserService.ForceChangePassword, since it has no response on success. Signed-off-by: SolarFactories --- user.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/user.go b/user.go index d9ee07b..534ba6d 100644 --- a/user.go +++ b/user.go @@ -50,13 +50,11 @@ func (us UserService) Login(ctx context.Context, username, password string) (tok body.Set("username", username) body.Set("password", password) - req, err := us.client.newRequest(ctx, http.MethodPost, "api/v1/user/login", withBody(body)) + req, err := us.client.newRequest(ctx, http.MethodPost, "api/v1/user/login", withBody(body), withAcceptContentType("text/plain")) if err != nil { return } - req.Header.Set("Accept", "*/*") - _, err = us.client.doRequest(req, &token) return } @@ -78,8 +76,6 @@ func (us UserService) ForceChangePassword(ctx context.Context, username, passwor return } - req.Header.Set("Accept", "*/*") - _, err = us.client.doRequest(req, nil) return } From ddaa92d8770a092843955c5550ccec41eef83424 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 24 May 2026 22:02:48 +0100 Subject: [PATCH 4/7] Replace use of slices.Contains, with it's implementation, since the slices package is Go 1.21+. Signed-off-by: SolarFactories --- client.go | 3 +-- util.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 0df9200..a8bf429 100644 --- a/client.go +++ b/client.go @@ -16,7 +16,6 @@ import ( "net/http/httputil" "net/url" "os" - "slices" "strconv" "strings" "time" @@ -355,7 +354,7 @@ func (c Client) doRequest(req *http.Request, v interface{}) (a apiResponse, err switch vt := v.(type) { case *string: expectedContentTypes := []string{"text/plain", "application/vnd.cyclonedx+json", "application/vnd.cyclonedx+xml"} - if !slices.Contains(expectedContentTypes, contentType) { + if !sliceContains(expectedContentTypes, contentType) { err = fmt.Errorf("Expected %s content-type. Received %s.", strings.Join(expectedContentTypes, ", "), contentType) return } diff --git a/util.go b/util.go index 50afb77..6576038 100644 --- a/util.go +++ b/util.go @@ -56,3 +56,13 @@ func OptionalBoolOf(value bool) *bool { func OptionalBool() *bool { return nil } + +func sliceContains[S ~[]E, E comparable](haystack S, needle E) bool { + for _, v := range haystack { + if v == needle { + return true + } + } + return false + +} From 955dbb9a6e7754e562b2631a281c25aade4112e8 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 24 May 2026 22:21:41 +0100 Subject: [PATCH 5/7] Marked UserService.ForceChangePassword to Accept text/plain, since the API is marked as producing text/plain. Signed-off-by: SolarFactories --- user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user.go b/user.go index 534ba6d..142f2a4 100644 --- a/user.go +++ b/user.go @@ -71,7 +71,7 @@ func (us UserService) ForceChangePassword(ctx context.Context, username, passwor body.Set("newPassword", newPassword) body.Set("confirmPassword", newPassword) - req, err := us.client.newRequest(ctx, http.MethodPost, "api/v1/user/forceChangePassword", withBody(body)) + req, err := us.client.newRequest(ctx, http.MethodPost, "api/v1/user/forceChangePassword", withBody(body), withAcceptContentType("text/plain")) if err != nil { return } From e0f69de3bb4b943881c3b539f23370e1f8384833 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Mon, 25 May 2026 14:08:28 +0100 Subject: [PATCH 6/7] Added splitting Content-Type Header on semicolon, to avoid comparing on Content-Type parameters. Signed-off-by: SolarFactories --- client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client.go b/client.go index a8bf429..cae4c51 100644 --- a/client.go +++ b/client.go @@ -351,6 +351,7 @@ func (c Client) doRequest(req *http.Request, v interface{}) (a apiResponse, err if v != nil { contentType := res.Header.Get("Content-Type") + contentType = strings.SplitN(contentType, ";", 2)[0] switch vt := v.(type) { case *string: expectedContentTypes := []string{"text/plain", "application/vnd.cyclonedx+json", "application/vnd.cyclonedx+xml"} From 81580ceb705c3f7c1478ecd9ed1930691bcceaaa Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Mon, 25 May 2026 14:37:27 +0100 Subject: [PATCH 7/7] Moved deterministic setting of Accept Header, to use withAcceptContentType, resolving lint issue on unparam. Signed-off-by: SolarFactories --- bom.go | 30 ++++++++++++++++-------------- client.go | 4 ++-- vex.go | 4 +--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bom.go b/bom.go index c40d725..7dd0fa6 100644 --- a/bom.go +++ b/bom.go @@ -54,16 +54,17 @@ func (bs BOMService) ExportComponent(ctx context.Context, componentUUID uuid.UUI params["format"] = string(format) } - req, err := bs.client.newRequest(ctx, http.MethodGet, fmt.Sprintf("api/v1/bom/cyclonedx/component/%s", componentUUID), withParams(params)) - if err != nil { - return - } - + var acceptContentType string switch format { case BOMFormatJSON: - req.Header.Set("Accept", "application/vnd.cyclonedx+json") + acceptContentType = "application/vnd.cyclonedx+json" case BOMFormatXML: - req.Header.Set("Accept", "application/vnd.cyclonedx+xml") + acceptContentType = "application/vnd.cyclonedx+xml" + } + + req, err := bs.client.newRequest(ctx, http.MethodGet, fmt.Sprintf("api/v1/bom/cyclonedx/component/%s", componentUUID), withParams(params), withAcceptContentType(acceptContentType)) + if err != nil { + return } _, err = bs.client.doRequest(req, &bom) @@ -79,16 +80,17 @@ func (bs BOMService) ExportProject(ctx context.Context, projectUUID uuid.UUID, f params["variant"] = string(variant) } - req, err := bs.client.newRequest(ctx, http.MethodGet, fmt.Sprintf("api/v1/bom/cyclonedx/project/%s", projectUUID), withParams(params)) - if err != nil { - return - } - + var acceptContentType string switch format { case BOMFormatJSON: - req.Header.Set("Accept", "application/vnd.cyclonedx+json") + acceptContentType = "application/vnd.cyclonedx+json" case BOMFormatXML: - req.Header.Set("Accept", "application/vnd.cyclonedx+xml") + acceptContentType = "application/vnd.cyclonedx+xml" + } + + req, err := bs.client.newRequest(ctx, http.MethodGet, fmt.Sprintf("api/v1/bom/cyclonedx/project/%s", projectUUID), withParams(params), withAcceptContentType(acceptContentType)) + if err != nil { + return } _, err = bs.client.doRequest(req, &bom) diff --git a/client.go b/client.go index cae4c51..3a12121 100644 --- a/client.go +++ b/client.go @@ -356,7 +356,7 @@ func (c Client) doRequest(req *http.Request, v interface{}) (a apiResponse, err case *string: expectedContentTypes := []string{"text/plain", "application/vnd.cyclonedx+json", "application/vnd.cyclonedx+xml"} if !sliceContains(expectedContentTypes, contentType) { - err = fmt.Errorf("Expected %s content-type. Received %s.", strings.Join(expectedContentTypes, ", "), contentType) + err = fmt.Errorf("expected %s content-type, but received %s", strings.Join(expectedContentTypes, ", "), contentType) return } if content, readErr := io.ReadAll(res.Body); readErr == nil { @@ -367,7 +367,7 @@ func (c Client) doRequest(req *http.Request, v interface{}) (a apiResponse, err } default: if contentType != "application/json" { - err = fmt.Errorf("Expected application/json content-type. Received %s.", contentType) + err = fmt.Errorf("expected application/json content-type, but received %s", contentType) return } err = json.NewDecoder(res.Body).Decode(v) diff --git a/vex.go b/vex.go index fa572e0..fe97ad3 100644 --- a/vex.go +++ b/vex.go @@ -26,13 +26,11 @@ type vexUploadResponse struct { type VEXUploadToken string func (vs VEXService) ExportCycloneDX(ctx context.Context, projectUUID uuid.UUID) (vex string, err error) { - req, err := vs.client.newRequest(ctx, http.MethodGet, fmt.Sprintf("api/v1/vex/cyclonedx/project/%s", projectUUID)) + req, err := vs.client.newRequest(ctx, http.MethodGet, fmt.Sprintf("api/v1/vex/cyclonedx/project/%s", projectUUID), withAcceptContentType("application/vnd.cyclonedx+json")) if err != nil { return } - req.Header.Set("Accept", "application/vnd.cyclonedx+json") - _, err = vs.client.doRequest(req, &vex) return }