Skip to content

Commit 35bc7ae

Browse files
authored
allow empty payloads in post,put,patch, issue #580 (#581)
* allow empty payloads in post,put,patch, issue #580 * remove content-length check in route detection * detect mismatched content-type #580 * more tests * remove 415 check in accept check * update hist
1 parent ffa1d91 commit 35bc7ae

File tree

4 files changed

+105
-18
lines changed

4 files changed

+105
-18
lines changed

CHANGES.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Change history of go-restful
22

3+
## [v3.12.2] - 2025-02-21
4+
5+
- allow empty payloads in post,put,patch, issue #580 ( thanks @liggitt, Jordan Liggitt)
36

47
## [v3.12.1] - 2024-05-28
58

jsr311.go

+3-16
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (RouterJSR311) extractParams(pathExpr *pathExpression, matches []string) ma
6565
return params
6666
}
6767

68-
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2
68+
// https://download.oracle.com/otndocs/jcp/jaxrs-1.1-mrel-eval-oth-JSpec/
6969
func (r RouterJSR311) detectRoute(routes []Route, httpRequest *http.Request) (*Route, error) {
7070
candidates := make([]*Route, 0, 8)
7171
for i, each := range routes {
@@ -126,9 +126,7 @@ func (r RouterJSR311) detectRoute(routes []Route, httpRequest *http.Request) (*R
126126
if trace {
127127
traceLogger.Printf("no Route found (from %d) that matches HTTP Content-Type: %s\n", len(previous), contentType)
128128
}
129-
if httpRequest.ContentLength > 0 {
130-
return nil, NewError(http.StatusUnsupportedMediaType, "415: Unsupported Media Type")
131-
}
129+
return nil, NewError(http.StatusUnsupportedMediaType, "415: Unsupported Media Type")
132130
}
133131

134132
// accept
@@ -151,20 +149,9 @@ func (r RouterJSR311) detectRoute(routes []Route, httpRequest *http.Request) (*R
151149
for _, candidate := range previous {
152150
available = append(available, candidate.Produces...)
153151
}
154-
// if POST,PUT,PATCH without body
155-
method, length := httpRequest.Method, httpRequest.Header.Get("Content-Length")
156-
if (method == http.MethodPost ||
157-
method == http.MethodPut ||
158-
method == http.MethodPatch) && (length == "" || length == "0") {
159-
return nil, NewError(
160-
http.StatusUnsupportedMediaType,
161-
fmt.Sprintf("415: Unsupported Media Type\n\nAvailable representations: %s", strings.Join(available, ", ")),
162-
)
163-
}
164152
return nil, NewError(
165153
http.StatusNotAcceptable,
166-
fmt.Sprintf("406: Not Acceptable\n\nAvailable representations: %s", strings.Join(available, ", ")),
167-
)
154+
fmt.Sprintf("406: Not Acceptable\n\nAvailable representations: %s", strings.Join(available, ", ")))
168155
}
169156
// return r.bestMatchByMedia(outputMediaOk, contentType, accept), nil
170157
return candidates[0], nil

route.go

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ func (r Route) matchesAccept(mimeTypesWithQuality string) bool {
111111
}
112112

113113
// Return whether this Route can consume content with a type specified by mimeTypes (can be empty).
114+
// If the route does not specify Consumes then return true (*/*).
115+
// If no content type is set then return true for GET,HEAD,OPTIONS,DELETE and TRACE.
114116
func (r Route) matchesContentType(mimeTypes string) bool {
115117

116118
if len(r.Consumes) == 0 {

web_service_test.go

+97-2
Original file line numberDiff line numberDiff line change
@@ -124,19 +124,113 @@ Available representations: text/plain, application/json`
124124
}
125125
}
126126

127-
func TestUnsupportedMedia_Issue492(t *testing.T) {
127+
func TestUnsupportedMedia_AcceptOnly(t *testing.T) {
128+
tearDown()
129+
Add(newPostTestService())
130+
for _, method := range []string{"POST", "PUT", "PATCH"} {
131+
httpRequest, _ := http.NewRequest(method, "http://here.com/test", nil) // no content
132+
httpRequest.Header.Set("Accept", "application/json")
133+
httpWriter := httptest.NewRecorder()
134+
DefaultContainer.dispatch(httpWriter, httpRequest)
135+
if http.StatusUnsupportedMediaType != httpWriter.Code {
136+
t.Errorf("[%s] 415 expected got %d", method, httpWriter.Code)
137+
}
138+
}
139+
}
140+
func TestUnsupportedMedia_AcceptOnlyWithZeroContentLength(t *testing.T) {
128141
tearDown()
129142
Add(newPostTestService())
130143
for _, method := range []string{"POST", "PUT", "PATCH"} {
131144
httpRequest, _ := http.NewRequest(method, "http://here.com/test", nil)
132145
httpRequest.Header.Set("Accept", "application/json")
146+
httpRequest.Header.Set("Content-length", "0")
133147
httpWriter := httptest.NewRecorder()
134148
DefaultContainer.dispatch(httpWriter, httpRequest)
135-
if 415 != httpWriter.Code {
149+
if http.StatusUnsupportedMediaType != httpWriter.Code {
136150
t.Errorf("[%s] 415 expected got %d", method, httpWriter.Code)
137151
}
138152
}
139153
}
154+
func TestUnsupportedMedia_ContentTypeOnly(t *testing.T) { // If Accept is not set then */* is used.
155+
tearDown()
156+
Add(newPostTestService())
157+
for _, method := range []string{"POST", "PUT", "PATCH"} {
158+
httpRequest, _ := http.NewRequest(method, "http://here.com/test", nil) // no content
159+
httpRequest.Header.Set("Content-type", "application/json")
160+
httpWriter := httptest.NewRecorder()
161+
DefaultContainer.dispatch(httpWriter, httpRequest)
162+
if http.StatusOK != httpWriter.Code {
163+
t.Errorf("[%s] 200 expected got %d", method, httpWriter.Code)
164+
}
165+
}
166+
}
167+
168+
func TestGetWithNonMatchingContentType(t *testing.T) { // If Accept is not set then */* is used.
169+
tearDown()
170+
Add(newGetOnlyJsonOnlyService())
171+
httpRequest, _ := http.NewRequest("GET", "http://here.com/get", nil) // no content
172+
httpRequest.Header.Set("Content-type", "application/yaml")
173+
httpWriter := httptest.NewRecorder()
174+
DefaultContainer.dispatch(httpWriter, httpRequest)
175+
if httpWriter.Code != http.StatusUnsupportedMediaType {
176+
t.Errorf("[%s] 415 expected got %d", "GET", httpWriter.Code)
177+
}
178+
}
179+
180+
func TestPostWithNonMatchingContentType(t *testing.T) { // If Accept is not set then */* is used.
181+
tearDown()
182+
Add(newPostNoConsumesService())
183+
httpRequest, _ := http.NewRequest("POST", "http://here.com/post", nil) // no content
184+
httpRequest.Header.Set("Content-type", "application/yaml")
185+
httpWriter := httptest.NewRecorder()
186+
DefaultContainer.dispatch(httpWriter, httpRequest)
187+
if httpWriter.Code != http.StatusNoContent {
188+
t.Errorf("[%s] 204 expected got %d", "POST", httpWriter.Code)
189+
}
190+
}
191+
192+
func TestPostWithNonMatchingAccept(t *testing.T) {
193+
tearDown()
194+
// consumes and produces JSON on POST,PUT and PATCH
195+
Add(newPostTestService())
196+
httpRequest, _ := http.NewRequest("POST", "http://here.com/test", nil) // no content
197+
httpRequest.Header.Set("Content-type", "application/json")
198+
httpRequest.Header.Set("Accept", "application/yaml")
199+
httpWriter := httptest.NewRecorder()
200+
DefaultContainer.dispatch(httpWriter, httpRequest)
201+
if httpWriter.Code != http.StatusNotAcceptable {
202+
t.Errorf("[%s] 406 expected got %d", "POST", httpWriter.Code)
203+
}
204+
}
205+
206+
func TestPostEmptyBody(t *testing.T) {
207+
tearDown()
208+
// consumes and produces JSON on POST,PUT and PATCH
209+
Add(newPostTestService())
210+
httpRequest, _ := http.NewRequest("POST", "http://here.com/test", nil) // no content
211+
httpRequest.Header.Set("Content-type", "application/json")
212+
httpRequest.Header.Set("Accept", "application/json")
213+
httpWriter := httptest.NewRecorder()
214+
DefaultContainer.dispatch(httpWriter, httpRequest)
215+
if httpWriter.Code != http.StatusOK {
216+
t.Errorf("[%s] 200 expected got %d", "POST", httpWriter.Code)
217+
}
218+
}
219+
220+
func TestPostEmptyBodyZeroContentLength(t *testing.T) {
221+
tearDown()
222+
// consumes and produces JSON on POST,PUT and PATCH
223+
Add(newPostTestService())
224+
httpRequest, _ := http.NewRequest("POST", "http://here.com/test", nil) // no content
225+
httpRequest.Header.Set("Content-type", "application/json")
226+
httpRequest.Header.Set("Content-length", "0")
227+
httpRequest.Header.Set("Accept", "application/json")
228+
httpWriter := httptest.NewRecorder()
229+
DefaultContainer.dispatch(httpWriter, httpRequest)
230+
if httpWriter.Code != http.StatusOK {
231+
t.Errorf("[%s] 200 expected got %d", "POST", httpWriter.Code)
232+
}
233+
}
140234

141235
func TestSelectedRoutePath_Issue100(t *testing.T) {
142236
tearDown()
@@ -390,6 +484,7 @@ func newPostNoConsumesService() *WebService {
390484
return ws
391485
}
392486

487+
// consumes and produces JSON on POST,PUT and PATCH
393488
func newPostTestService() *WebService {
394489
ws := new(WebService).Path("")
395490
ws.Consumes("application/json")

0 commit comments

Comments
 (0)