Repository: zeromicro/go-zero
Pull request #1713 (2022-03-25 09:44:23)

Added support for setting the parameter size accepted by the interface and custom timeout and maxbytes in API file
It can be used like this
```go
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
	server.AddRoutes(
		[]rest.Route{
			{
				Method:  http.MethodPost,
				Path:    "/upload",
				Handler: UploadHandler(serverCtx),
			},
		},
		rest.WithMaxBytes(52428800),
	)
}
```

custom timeout and maxbytes in api file can be used like this
The unit of timeout is `time.Second`

```go
@server(
	timeout: 2
	maxBytes: 1234
	group: user
)
service user {
	@handler getUser // etst
	get /users/id (request) returns (response)
}
```

general code 
```go
// Code generated by goctl. DO NOT EDIT.
package handler

import (
	"net/http"
	"time"

	user "joj-backend/api/internal/handler/user"
	"joj-backend/api/internal/svc"

	"github.com/zeromicro/go-zero/rest"
)

func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
	server.AddRoutes(
		[]rest.Route{
			{
				Method:  http.MethodGet,
				Path:    "/users/id",
				Handler: user.GetUserHandler(serverCtx),
			},
		}, rest.WithTimeout(2*time.Second), rest.WithMaxBytes(1234),
	)
}
```


chenquan: I think you just need to use MaxBytes in ResConf.
jiang4869: > I think you just need to use MaxBytes in ResConf.
If maxbytes is set in the configuration file, all interfaces are the same. But sometimes you just want to set the upload file interface larger.


fynxiu: Now that you've chosen microservices, think in terms of microservices. Would it be a better idea to have the uploading module as a separate service?
jiang4869: > Now that you've chosen microservices, think in terms of microservices. Would it be a better idea to have the uploading module as a separate service?

Using API as the gateway of micro service
jiang4869: > `timeout: 2`
> 
> I prefer to use `timeout: 2s`, in this case, we can support like `200ms`.
> 
> Also, if service level, just set it in `yaml` files.

added time units?

chenquan: My thoughts:

1. Don't need  to define `timeout` and `maxBytes` in `@server`
2. Use ResConf can be in the service level.
3. Keep ` withmaxbytes (maxbytes Int64) ` for user customization.

Patch:
diff --git a/rest/engine.go b/rest/engine.go
index 86e8dead..bdc982bd 100644
--- a/rest/engine.go
+++ b/rest/engine.go
@@ -94,7 +94,7 @@ func (ng *engine) bindRoute(fr featuredRoutes, router httpx.Router, metrics *sta
 		handler.TimeoutHandler(ng.checkedTimeout(fr.timeout)),
 		handler.RecoverHandler,
 		handler.MetricHandler(metrics),
-		handler.MaxBytesHandler(ng.conf.MaxBytes),
+		handler.MaxBytesHandler(ng.checkedMaxBytes(fr.maxBytes)),
 		handler.GunzipHandler,
 	)
 	chain = ng.appendAuthHandler(fr, chain, verifier)
@@ -127,6 +127,15 @@ func (ng *engine) checkedTimeout(timeout time.Duration) time.Duration {
 	return time.Duration(ng.conf.Timeout) * time.Millisecond
 }
 
+func (ng *engine) checkedMaxBytes(bytes int64) int64 {
+
+	if bytes > 0 {
+		return bytes
+	}
+
+	return ng.conf.MaxBytes
+}
+
 func (ng *engine) createMetrics() *stat.Metrics {
 	var metrics *stat.Metrics
 
diff --git a/rest/server.go b/rest/server.go
index 793faffa..8839097c 100644
--- a/rest/server.go
+++ b/rest/server.go
@@ -223,6 +223,13 @@ func WithTimeout(timeout time.Duration) RouteOption {
 	}
 }
 
+// WithMaxBytes returns a RouteOption to set maxBytes with given value.
+func WithMaxBytes(maxBytes int64) RouteOption {
+	return func(r *featuredRoutes) {
+		r.maxBytes = maxBytes
+	}
+}
+
 // WithTLSConfig returns a RunOption that with given tls config.
 func WithTLSConfig(cfg *tls.Config) RunOption {
 	return func(svr *Server) {
diff --git a/rest/types.go b/rest/types.go
index cc4636c5..f7be7996 100644
--- a/rest/types.go
+++ b/rest/types.go
@@ -36,5 +36,6 @@ type (
 		jwt       jwtSetting
 		signature signatureSetting
 		routes    []Route
+		maxBytes  int64
 	}
 )
diff --git a/tools/goctl/api/format/format.go b/tools/goctl/api/format/format.go
index 02cbd222..e7283b47 100644
--- a/tools/goctl/api/format/format.go
+++ b/tools/goctl/api/format/format.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"go/format"
 	"go/scanner"
+	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -29,14 +30,14 @@ const (
 func GoFormatApi(c *cli.Context) error {
 	useStdin := c.Bool("stdin")
 	skipCheckDeclare := c.Bool("declare")
+	dir := c.String("dir")
 
 	var be errorx.BatchError
 	if useStdin {
-		if err := apiFormatByStdin(skipCheckDeclare); err != nil {
+		if err := apiFormatReader(os.Stdin, dir, skipCheckDeclare); err != nil {
 			be.Add(err)
 		}
 	} else {
-		dir := c.String("dir")
 		if len(dir) == 0 {
 			return errors.New("missing -dir")
 		}
@@ -65,13 +66,14 @@ func GoFormatApi(c *cli.Context) error {
 	return be.Err()
 }
 
-func apiFormatByStdin(skipCheckDeclare bool) error {
-	data, err := ioutil.ReadAll(os.Stdin)
+// apiFormatReader
+// filename is needed when there are `import` literals.
+func apiFormatReader(reader io.Reader, filename string, skipCheckDeclare bool) error {
+	data, err := ioutil.ReadAll(reader)
 	if err != nil {
 		return err
 	}
-
-	result, err := apiFormat(string(data), skipCheckDeclare)
+	result, err := apiFormat(string(data), skipCheckDeclare, filename)
 	if err != nil {
 		return err
 	}
diff --git a/tools/goctl/api/gogen/genroutes.go b/tools/goctl/api/gogen/genroutes.go
index 24161360..2df6c78b 100644
--- a/tools/goctl/api/gogen/genroutes.go
+++ b/tools/goctl/api/gogen/genroutes.go
@@ -7,6 +7,7 @@ import (
 	"sort"
 	"strings"
 	"text/template"
+	"time"
 
 	"github.com/zeromicro/go-zero/core/collection"
 	"github.com/zeromicro/go-zero/tools/goctl/api/spec"
@@ -34,7 +35,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
 `
 	routesAdditionTemplate = `
 	server.AddRoutes(
-		{{.routes}} {{.jwt}}{{.signature}} {{.prefix}}
+		{{.routes}} {{.jwt}}{{.signature}} {{.prefix}} {{.timeout}}
 	)
 `
 )
@@ -57,6 +58,8 @@ type (
 		jwtEnabled       bool
 		signatureEnabled bool
 		authName         string
+		timeout          string
+		timeoutEnable    bool
 		middlewares      []string
 		prefix           string
 		jwtTrans         string
@@ -110,6 +113,15 @@ func genRoutes(dir, rootPkg string, cfg *config.Config, api *spec.ApiSpec) error
 rest.WithPrefix("%s"),`, g.prefix)
 		}
 
+		var timeout string
+		if g.timeoutEnable {
+			duration, err := time.ParseDuration(g.timeout)
+			if err != nil {
+				panic(err)
+			}
+			timeout = fmt.Sprintf("rest.WithTimeout(%d),", duration)
+		}
+
 		var routes string
 		if len(g.middlewares) > 0 {
 			gbuilder.WriteString("\n}...,")
@@ -130,6 +142,7 @@ rest.WithPrefix("%s"),`, g.prefix)
 			"jwt":       jwt,
 			"signature": signature,
 			"prefix":    prefix,
+			"timeout":   timeout,
 		}); err != nil {
 			return err
 		}
@@ -205,6 +218,13 @@ func getRoutes(api *spec.ApiSpec) ([]group, error) {
 			})
 		}
 
+		timeout := g.GetAnnotation("timeout")
+
+		if len(timeout) > 0 {
+			groupedRoutes.timeoutEnable = true
+			groupedRoutes.timeout = timeout
+		}
+
 		jwt := g.GetAnnotation("jwt")
 		if len(jwt) > 0 {
 			groupedRoutes.authName = jwt


Test patch:
diff --git a/rest/engine_test.go b/rest/engine_test.go
index ca540149..8b8ff528 100644
--- a/rest/engine_test.go
+++ b/rest/engine_test.go
@@ -194,6 +194,41 @@ func TestEngine_checkedTimeout(t *testing.T) {
 	}
 }
 
+func TestEngine_checkedMaxBytes(t *testing.T) {
+	tests := []struct {
+		name     string
+		maxBytes int64
+		expect   int64
+	}{
+		{
+			name:   "not set",
+			expect: 1000,
+		},
+		{
+			name:     "less",
+			maxBytes: 500,
+			expect:   500,
+		},
+		{
+			name:     "equal",
+			maxBytes: 1000,
+			expect:   1000,
+		},
+		{
+			name:     "more",
+			maxBytes: 1500,
+			expect:   1500,
+		},
+	}
+
+	ng := newEngine(RestConf{
+		MaxBytes: 1000,
+	})
+	for _, test := range tests {
+		assert.Equal(t, test.expect, ng.checkedMaxBytes(test.maxBytes))
+	}
+}
+
 func TestEngine_notFoundHandler(t *testing.T) {
 	logx.Disable()
 
diff --git a/tools/goctl/api/format/format_test.go b/tools/goctl/api/format/format_test.go
index 57bda187..f171994d 100644
--- a/tools/goctl/api/format/format_test.go
+++ b/tools/goctl/api/format/format_test.go
@@ -1,15 +1,21 @@
 package format
 
 import (
+	"fmt"
+	"io/fs"
+	"io/ioutil"
+	"os"
+	"path"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 const (
 	notFormattedStr = `
 type Request struct {
-  Name string ` + "`" + `path:"name,options=you|me"` + "`" + `  
+  Name string ` + "`" + `path:"name,options=you|me"` + "`" + `
 }
 type Response struct {
   Message string ` + "`" + `json:"message"` + "`" + `
@@ -45,3 +51,26 @@ func TestFormat(t *testing.T) {
 	_, err = apiFormat(notFormattedStr, false)
 	assert.Errorf(t, err, " line 7:13 can not found declaration 'Student' in context")
 }
+
+func Test_apiFormatReader_issue1721(t *testing.T) {
+	dir, err := os.MkdirTemp("", "goctl-api-format")
+	require.NoError(t, err)
+	defer os.RemoveAll(dir)
+	subDir := path.Join(dir, "sub")
+	err = os.MkdirAll(subDir, fs.ModePerm)
+	require.NoError(t, err)
+
+	importedFilename := path.Join(dir, "foo.api")
+	err = ioutil.WriteFile(importedFilename, []byte{}, fs.ModePerm)
+	require.NoError(t, err)
+
+	filename := path.Join(subDir, "bar.api")
+	err = ioutil.WriteFile(filename, []byte(fmt.Sprintf(`import "%s"`, importedFilename)), 0644)
+	require.NoError(t, err)
+
+	f, err := os.Open(filename)
+	require.NoError(t, err)
+
+	err = apiFormatReader(f, filename, false)
+	assert.NoError(t, err)
+}