Repository: swaggest/rest
Pull request #158 (2023-05-25 12:35:35)

Only application/json should be allowed by default
According to https://github.com/swaggest/rest/issues/113 valid JSON Content-Type is application/json.

According to https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 Content-Type values should be compared in case-insensitive manner.

Fixes: https://github.com/swaggest/rest/issues/134
Author-Change-Id: IB#1131224

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/swaggest/rest/pull/158?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#158](https://app.codecov.io/gh/swaggest/rest/pull/158?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (6fefa4e) into [master](https://app.codecov.io/gh/swaggest/rest/commit/dee7f9fac2a11960641dbaf11f78524375129e23?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (dee7f9f) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master     #158   +/-   ##
=======================================
  Coverage   77.79%   77.79%           
=======================================
  Files          28       28           
  Lines        1608     1608           
=======================================
  Hits         1251     1251           
  Misses        235      235           
  Partials      122      122           
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.79% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://app.codecov.io/gh/swaggest/rest/pull/158?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [request/jsonbody.go](https://app.codecov.io/gh/swaggest/rest/pull/158?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-cmVxdWVzdC9qc29uYm9keS5nbw==) | `97.29% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)


Patch:
diff --git a/_examples/advanced-generic/file_upload.go b/_examples/advanced-generic/file_upload.go
index caf37d1..be820a2 100644
--- a/_examples/advanced-generic/file_upload.go
+++ b/_examples/advanced-generic/file_upload.go
@@ -31,6 +31,10 @@ func fileUploader() usecase.Interactor {
 	u := usecase.NewInteractor(func(ctx context.Context, in upload, out *info) (err error) {
 		out.Query = in.Query
 		out.Simple = in.Simple
+		if in.Upload1 == nil {
+			return nil
+		}
+
 		out.Filename = in.Upload1.Filename
 		out.Header = in.Upload1.Header
 		out.Size = in.Upload1.Size
diff --git a/_examples/go.mod b/_examples/go.mod
index 633ccbb..75268f7 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -7,13 +7,13 @@ replace github.com/swaggest/rest => ../
 require (
 	github.com/bool64/ctxd v1.2.1
 	github.com/bool64/dev v0.2.27
-	github.com/bool64/httpmock v0.1.10
+	github.com/bool64/httpmock v0.1.13
 	github.com/bool64/httptestbench v0.1.4
 	github.com/go-chi/chi/v5 v5.0.8
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/rs/cors v1.9.0
 	github.com/stretchr/testify v1.8.2
-	github.com/swaggest/assertjson v1.8.0
+	github.com/swaggest/assertjson v1.8.1
 	github.com/swaggest/jsonschema-go v0.3.51
 	github.com/swaggest/openapi-go v0.2.30
 	github.com/swaggest/swgui v1.6.2
diff --git a/_examples/go.sum b/_examples/go.sum
index 3fb0541..85de626 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -6,8 +6,8 @@ github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zR
 github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.27 h1:mFT+B74mFVgUeUmm/EbfM6ELPA55lEXBjQ/AOHCwCOc=
 github.com/bool64/dev v0.2.27/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
-github.com/bool64/httpmock v0.1.10 h1:cP+JS+nuIe47uwSheiKtoesziyacxbvWfOVo1zczbek=
-github.com/bool64/httpmock v0.1.10/go.mod h1:vodDyrRL4hfWZ6/nvahVzSV4VSK6MSPf0MxyHNQXzuA=
+github.com/bool64/httpmock v0.1.13 h1:3QpRXQ5kwHLW8xnVT8+Ug7VS6RerhdEFV+RWYC61aVo=
+github.com/bool64/httpmock v0.1.13/go.mod h1:YMTLaypQ3o5DAx78eA/kDRSLec0f+42sLMDmHdmeY+E=
 github.com/bool64/httptestbench v0.1.4 h1:35f9RwWqcnQRXM+sA+GUhWVGSa6XEFlKpNSH9oYzOjI=
 github.com/bool64/httptestbench v0.1.4/go.mod h1:b9ItXGtscs9AcR5oUDnWCTLosQ/DCjiG4euDbHN8T40=
 github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
@@ -55,8 +55,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/swaggest/assertjson v1.8.0 h1:XSg4p6iOZMjtpV2tW2SXfD1GsOOTsWcm+sOADODu/DU=
-github.com/swaggest/assertjson v1.8.0/go.mod h1:/8kNRmDZAZfavS5VeWYtCimLGebn0Ak1/iErFUi+DEM=
+github.com/swaggest/assertjson v1.8.1 h1:Be2EHY9S2qwKWV+xWZB747Cd7Y79YK6JLdeyrgFvyMo=
+github.com/swaggest/assertjson v1.8.1/go.mod h1:/8kNRmDZAZfavS5VeWYtCimLGebn0Ak1/iErFUi+DEM=
 github.com/swaggest/form/v5 v5.0.4 h1:hTg+gjUEZG0vgsDdXOPi7I0Xu5OowXKoLZIGYZsoddg=
 github.com/swaggest/form/v5 v5.0.4/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
 github.com/swaggest/jsonschema-go v0.3.51 h1:Cl0hFQ/jtBIP8NlHNuwW6ka3J7zzW5r2jxbLSCUByGY=
diff --git a/go.mod b/go.mod
index 0c5e4d8..ae3ca16 100644
--- a/go.mod
+++ b/go.mod
@@ -4,13 +4,13 @@ go 1.17
 
 require (
 	github.com/bool64/dev v0.2.27
-	github.com/bool64/httpmock v0.1.10
+	github.com/bool64/httpmock v0.1.13
 	github.com/bool64/shared v0.1.5
 	github.com/cespare/xxhash/v2 v2.2.0
 	github.com/go-chi/chi/v5 v5.0.8
 	github.com/santhosh-tekuri/jsonschema/v3 v3.1.0
 	github.com/stretchr/testify v1.8.2
-	github.com/swaggest/assertjson v1.8.0
+	github.com/swaggest/assertjson v1.8.1
 	github.com/swaggest/form/v5 v5.0.4
 	github.com/swaggest/jsonschema-go v0.3.51
 	github.com/swaggest/openapi-go v0.2.30
diff --git a/go.sum b/go.sum
index 59664dd..65ca5a6 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/bool64/dev v0.2.24/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8
 github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.27 h1:mFT+B74mFVgUeUmm/EbfM6ELPA55lEXBjQ/AOHCwCOc=
 github.com/bool64/dev v0.2.27/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
-github.com/bool64/httpmock v0.1.10 h1:cP+JS+nuIe47uwSheiKtoesziyacxbvWfOVo1zczbek=
-github.com/bool64/httpmock v0.1.10/go.mod h1:vodDyrRL4hfWZ6/nvahVzSV4VSK6MSPf0MxyHNQXzuA=
+github.com/bool64/httpmock v0.1.13 h1:3QpRXQ5kwHLW8xnVT8+Ug7VS6RerhdEFV+RWYC61aVo=
+github.com/bool64/httpmock v0.1.13/go.mod h1:YMTLaypQ3o5DAx78eA/kDRSLec0f+42sLMDmHdmeY+E=
 github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
 github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -68,8 +68,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/swaggest/assertjson v1.8.0 h1:XSg4p6iOZMjtpV2tW2SXfD1GsOOTsWcm+sOADODu/DU=
 github.com/swaggest/assertjson v1.8.0/go.mod h1:/8kNRmDZAZfavS5VeWYtCimLGebn0Ak1/iErFUi+DEM=
+github.com/swaggest/assertjson v1.8.1 h1:Be2EHY9S2qwKWV+xWZB747Cd7Y79YK6JLdeyrgFvyMo=
+github.com/swaggest/assertjson v1.8.1/go.mod h1:/8kNRmDZAZfavS5VeWYtCimLGebn0Ak1/iErFUi+DEM=
 github.com/swaggest/form/v5 v5.0.4 h1:hTg+gjUEZG0vgsDdXOPi7I0Xu5OowXKoLZIGYZsoddg=
 github.com/swaggest/form/v5 v5.0.4/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
 github.com/swaggest/jsonschema-go v0.3.50/go.mod h1:QfUB5HaZ8y5TiFtCPhM7QwvPNKxTsYxDJaLHTLq6jgU=
diff --git a/request/file.go b/request/file.go
index 0bd2e38..049f4b0 100644
--- a/request/file.go
+++ b/request/file.go
@@ -75,6 +75,8 @@ func setFile(r *http.Request, field reflect.StructField, v reflect.Value) error
 			if field.Tag.Get("required") == "true" {
 				return fmt.Errorf("%w: %q", ErrMissingRequiredFile, name)
 			}
+
+			return nil
 		}
 
 		return fmt.Errorf("failed to get file %q from request: %w", name, err)
diff --git a/request/jsonbody.go b/request/jsonbody.go
index cf44c11..ec40072 100644
--- a/request/jsonbody.go
+++ b/request/jsonbody.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"strings"
 	"sync"
 
 	"github.com/swaggest/rest"
@@ -71,7 +72,7 @@ func checkJSONBodyContentType(contentType string, tolerateFormData bool) (ret bo
 		return false, nil
 	}
 
-	if len(contentType) < 16 || contentType[0:16] != "application/json" { // allow 'application/json;charset=UTF-8'
+	if strings.ToLower(contentType) != "application/json" {
 		if tolerateFormData && (contentType == "application/x-www-form-urlencoded" || contentType == "multipart/form-data") {
 			return true, nil
 		}
diff --git a/response/encoder.go b/response/encoder.go
index ceeda6f..0754809 100644
--- a/response/encoder.go
+++ b/response/encoder.go
@@ -197,11 +197,13 @@ func (h *Encoder) SetupOutput(output interface{}, ht *rest.HandlerTrait) {
 func (h *Encoder) successStatus(output interface{}) int {
 	if outputWithStatus, ok := output.(rest.OutputWithHTTPStatus); ok {
 		return outputWithStatus.HTTPStatus()
-	} else if h.skipRendering && !h.outputWithWriter {
+	}
+
+	if h.skipRendering && !h.outputWithWriter {
 		return http.StatusNoContent
-	} else {
-		return http.StatusOK
 	}
+
+	return http.StatusOK
 }
 
 type jsonEncoder struct {


Test patch:
diff --git a/request/file_test.go b/request/file_test.go
index 10c7cfb..09b776f 100644
--- a/request/file_test.go
+++ b/request/file_test.go
@@ -19,10 +19,12 @@ import (
 	"github.com/swaggest/rest/openapi"
 	"github.com/swaggest/rest/request"
 	"github.com/swaggest/rest/response"
+	"github.com/swaggest/rest/web"
 	"github.com/swaggest/usecase"
 )
 
 type ReqEmb struct {
+	Simple         string                  `formData:"simple"`
 	UploadHeader   *multipart.FileHeader   `formData:"upload"`
 	UploadsHeaders []*multipart.FileHeader `formData:"uploads"`
 }
@@ -33,7 +35,32 @@ type fileReqTest struct {
 	Uploads []multipart.File `formData:"uploads"`
 }
 
-func TestMapper_Decode_fileUploadTag(t *testing.T) {
+func TestDecoder_Decode_fileUploadOptional(t *testing.T) {
+	u := usecase.NewIOI(new(ReqEmb), nil, func(ctx context.Context, input, output interface{}) error {
+		return nil
+	})
+
+	s := web.DefaultService()
+	s.Post("/", u)
+
+	b := bytes.NewBuffer(nil)
+	w := multipart.NewWriter(b)
+	require.NoError(t, w.WriteField("simple", "def"))
+	require.NoError(t, w.Close())
+
+	req, err := http.NewRequest(http.MethodPost, "/", b)
+	require.NoError(t, err)
+
+	req.Header.Set("Content-Type", w.FormDataContentType())
+
+	rw := httptest.NewRecorder()
+	s.ServeHTTP(rw, req)
+
+	assert.Equal(t, http.StatusNoContent, rw.Code)
+	assert.Equal(t, ``, rw.Body.String())
+}
+
+func TestDecoder_Decode_fileUploadTag(t *testing.T) {
 	r := chirouter.NewWrapper(chi.NewRouter())
 	apiSchema := openapi.Collector{}
 	decoderFactory := request.NewDecoderFactory()