diff --git a/README.md b/README.md index fc1d01cf..7e8359ee 100644 --- a/README.md +++ b/README.md @@ -178,8 +178,8 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr - [x] Zero allocation router. - [x] Still the fastest http router and framework. From routing to writing. -- [x] Complete suite of unit tests -- [x] Battle tested +- [x] Complete suite of unit tests. +- [x] Battle tested. - [x] API frozen, new releases will not break your code. ## Build with [jsoniter](https://github.com/json-iterator/go) @@ -340,7 +340,7 @@ func main() { ``` ``` -ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] +ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou] ``` ### Upload files @@ -1255,6 +1255,7 @@ func main() { } reader := response.Body + defer reader.Close() contentLength := response.ContentLength contentType := response.Header.Get("Content-Type") diff --git a/context.go b/context.go index 95b1807d..71fb5937 100644 --- a/context.go +++ b/context.go @@ -295,6 +295,22 @@ func (c *Context) GetInt64(key string) (i64 int64) { return } +// GetUint returns the value associated with the key as an unsigned integer. +func (c *Context) GetUint(key string) (ui uint) { + if val, ok := c.Get(key); ok && val != nil { + ui, _ = val.(uint) + } + return +} + +// GetUint64 returns the value associated with the key as an unsigned integer. +func (c *Context) GetUint64(key string) (ui64 uint64) { + if val, ok := c.Get(key); ok && val != nil { + ui64, _ = val.(uint64) + } + return +} + // GetFloat64 returns the value associated with the key as a float64. func (c *Context) GetFloat64(key string) (f64 float64) { if val, ok := c.Get(key); ok && val != nil { @@ -957,7 +973,7 @@ func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } -// FileFromFS writes the specified file from http.FileSytem into the body stream in an efficient way. +// FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way. func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { defer func(old string) { c.Request.URL.Path = old @@ -971,7 +987,7 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { // FileAttachment writes the specified file into the body stream in an efficient way // On the client side, the file will typically be downloaded with the given filename func (c *Context) FileAttachment(filepath, filename string) { - c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) http.ServeFile(c.Writer, c.Request, filepath) } diff --git a/context_test.go b/context_test.go index 1a5a3c5f..8e1e3b57 100644 --- a/context_test.go +++ b/context_test.go @@ -261,6 +261,18 @@ func TestContextGetInt64(t *testing.T) { assert.Equal(t, int64(42424242424242), c.GetInt64("int64")) } +func TestContextGetUint(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("uint", uint(1)) + assert.Equal(t, uint(1), c.GetUint("uint")) +} + +func TestContextGetUint64(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("uint64", uint64(18446744073709551615)) + assert.Equal(t, uint64(18446744073709551615), c.GetUint64("uint64")) +} + func TestContextGetFloat64(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Set("float64", 4.2) @@ -1270,7 +1282,7 @@ func TestContextIsAborted(t *testing.T) { assert.True(t, c.IsAborted()) } -// TestContextData tests that the response can be written from `bytesting` +// TestContextData tests that the response can be written from `bytestring` // with specified MIME type func TestContextAbortWithStatus(t *testing.T) { w := httptest.NewRecorder() diff --git a/errors.go b/errors.go index 9a317992..0f276c13 100644 --- a/errors.go +++ b/errors.go @@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool { return (msg.Type & flags) > 0 } +// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap() +func (msg *Error) Unwrap() error { + return msg.Err +} + // ByType returns a readonly copy filtered the byte. // ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic. func (a errorMsgs) ByType(typ ErrorType) errorMsgs { diff --git a/errors_1.13_test.go b/errors_1.13_test.go new file mode 100644 index 00000000..a8f9a94e --- /dev/null +++ b/errors_1.13_test.go @@ -0,0 +1,33 @@ +// +build go1.13 + +package gin + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestErr string + +func (e TestErr) Error() string { return string(e) } + +// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()". +// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13, +// hence the "// +build go1.13" directive at the beginning of this file. +func TestErrorUnwrap(t *testing.T) { + innerErr := TestErr("somme error") + + // 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr + err := fmt.Errorf("wrapped: %w", &Error{ + Err: innerErr, + Type: ErrorTypeAny, + }) + + // check that 'errors.Is()' and 'errors.As()' behave as expected : + assert.True(t, errors.Is(err, innerErr)) + var testErr TestErr + assert.True(t, errors.As(err, &testErr)) +} diff --git a/recovery.go b/recovery.go index d02b829b..563f5aaa 100644 --- a/recovery.go +++ b/recovery.go @@ -76,11 +76,12 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { headers[idx] = current[0] + ": *" } } + headersToStr := strings.Join(headers, "\r\n") if brokenPipe { - logger.Printf("%s\n%s%s", err, string(httpRequest), reset) + logger.Printf("%s\n%s%s", err, headersToStr, reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset) + timeFormat(time.Now()), headersToStr, err, stack, reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset) diff --git a/render/text.go b/render/text.go index 30f5f532..461b720a 100644 --- a/render/text.go +++ b/render/text.go @@ -7,6 +7,8 @@ package render import ( "fmt" "net/http" + + "github.com/gin-gonic/gin/internal/bytesconv" ) // String contains the given interface object slice and its format. @@ -34,6 +36,6 @@ func WriteString(w http.ResponseWriter, format string, data []interface{}) (err _, err = fmt.Fprintf(w, format, data...) return } - _, err = w.Write([]byte(format)) + _, err = w.Write(bytesconv.StringToBytes(format)) return } diff --git a/utils.go b/utils.go index fab3aee3..c32f0eeb 100644 --- a/utils.go +++ b/utils.go @@ -103,7 +103,10 @@ func parseAccept(acceptHeader string) []string { parts := strings.Split(acceptHeader, ",") out := make([]string, 0, len(parts)) for _, part := range parts { - if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" { + if i := strings.IndexByte(part, ';'); i > 0 { + part = part[:i] + } + if part = strings.TrimSpace(part); part != "" { out = append(out, part) } } diff --git a/utils_test.go b/utils_test.go index 9b57c57b..cc486c35 100644 --- a/utils_test.go +++ b/utils_test.go @@ -18,6 +18,12 @@ func init() { SetMode(TestMode) } +func BenchmarkParseAccept(b *testing.B) { + for i := 0; i < b.N; i++ { + parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8") + } +} + type testStruct struct { T *testing.T }