@@ -54,4 +54,119 @@ class ErrorHandlingTest extends Specification {
5454 RegistryErrorResponse error = exception. response. getBody(RegistryErrorResponse ). get()
5555 error. errors. get(0 ). message == " repository 'quay.io/hello-world:latest' not found"
5656 }
57+
58+ void ' should NEVER expose stack traces in error responses' () {
59+ given : ' various types of invalid requests'
60+ def requests = [
61+ HttpRequest . POST (" /v1alpha2/container" , ' {"invalid json' ). contentType(" application/json" ),
62+ HttpRequest . POST (" /v1alpha2/container" , ' []' ). contentType(" application/json" ),
63+ HttpRequest . POST (" /v1alpha2/container" , ' {"test": invalid}' ). contentType(" application/json" ),
64+ HttpRequest . POST (" /v1alpha2/container" , ' {"packages":{"type":"INVALID"}}' ). contentType(" application/json" ),
65+ ]
66+
67+ expect : ' no stack traces in any error response'
68+ requests. each { request ->
69+ try {
70+ client. toBlocking(). exchange(request, String )
71+ assert false , " Request should have failed"
72+ } catch (HttpClientResponseException ex) {
73+ def body = ex. response. getBody(String ). orElse(" " )
74+
75+ // CRITICAL: No stack traces
76+ assert ! body. contains(' at io.' ), " Stack trace with 'at io.' found"
77+ assert ! body. contains(' at com.' ), " Stack trace with 'at com.' found"
78+ assert ! body. contains(' at java.' ), " Stack trace with 'at java.' found"
79+ assert ! body. contains(' .java:' ), " Stack trace with file:line found"
80+ assert ! body. contains(' .groovy:' ), " Stack trace with groovy file found"
81+ assert ! body. contains(' Caused by:' ), " Stack trace with 'Caused by' found"
82+ assert ! body. contains(' StackTrace' ), " StackTrace term found"
83+ assert ! body. contains(' Exception in thread' ), " Exception stack found"
84+ }
85+ }
86+ }
87+
88+ void ' should NOT expose internal package names' () {
89+ given : ' various types of invalid requests'
90+ def requests = [
91+ HttpRequest . POST (" /v1alpha2/container" , ' {"bad"}' ). contentType(" application/json" ),
92+ HttpRequest . POST (" /v1alpha2/container" , ' {"containerFile": 999}' ). contentType(" application/json" ),
93+ ]
94+
95+ expect : ' no internal package names exposed'
96+ requests. each { request ->
97+ try {
98+ client. toBlocking(). exchange(request, String )
99+ assert false , " Request should have failed"
100+ } catch (HttpClientResponseException ex) {
101+ def body = ex. response. getBody(String ). orElse(" " )
102+
103+ // No internal package structures
104+ assert ! body. contains(' io.seqera.wave' ), " Internal package io.seqera.wave exposed"
105+ assert ! body. contains(' com.fasterxml.jackson' ), " Jackson package exposed"
106+ assert ! body. contains(' jakarta.validation' ), " Jakarta validation package exposed"
107+ }
108+ }
109+ }
110+
111+ void ' should NOT expose enum values' () {
112+ given : ' request with invalid enum value'
113+ def request = HttpRequest . POST (" /v1alpha2/container" , '''
114+ {
115+ "containerImage": "ubuntu:latest",
116+ "packages": {
117+ "type": "INVALID_ENUM_VALUE",
118+ "entries": []
119+ }
120+ }
121+ ''' ). contentType(" application/json" )
122+
123+ when : ' the request is sent'
124+ client. toBlocking(). exchange(request, String )
125+
126+ then : ' exception is thrown'
127+ def ex = thrown(HttpClientResponseException )
128+
129+ and : ' enum values are not exposed'
130+ def body = ex. response. getBody(String ). get()
131+ ! body. contains(' [CONDA, SPACK' ) // Enum value list
132+ ! body. contains(' BIOCONDA' )
133+ ! body. contains(' PackageType' ) // Class name
134+ }
135+
136+ void ' should NOT expose Java exception class names' () {
137+ given : ' invalid JSON request'
138+ def request = HttpRequest . POST (" /v1alpha2/container" , ' {"test"' )
139+ .contentType(" application/json" )
140+
141+ when : ' the request is sent'
142+ client. toBlocking(). exchange(request, String )
143+
144+ then : ' exception is thrown'
145+ def ex = thrown(HttpClientResponseException )
146+
147+ and : ' exception class names are not exposed'
148+ def body = ex. response. getBody(String ). get()
149+ ! body. contains(' JsonProcessingException' )
150+ ! body. contains(' JsonParseException' )
151+ ! body. contains(' JsonMappingException' )
152+ ! body. contains(' ConstraintViolationException' )
153+ ! body. contains(' IllegalArgumentException' )
154+ }
155+
156+ void ' error responses should include correlation ID' () {
157+ given : ' invalid JSON request'
158+ def request = HttpRequest . POST (" /v1alpha2/container" , ' {"x"' )
159+ .contentType(" application/json" )
160+
161+ when : ' the request is sent'
162+ client. toBlocking(). exchange(request, String )
163+
164+ then : ' exception is thrown'
165+ def ex = thrown(HttpClientResponseException )
166+
167+ and : ' response contains a correlation ID (request ID or error ID)'
168+ def body = ex. response. getBody(String ). get()
169+ body. contains(' requestId' ) || body. contains(' Error ID' )
170+ body ==~ / .*[a-f0-9]{10,}.*/ // Contains hex ID for correlation
171+ }
57172}
0 commit comments