├── go.mod ├── .github └── workflows │ ├── release-please.yaml │ └── test-fmt.yaml ├── .gitignore ├── examples ├── simple_style │ └── simple_style.go ├── request_style │ └── request_style.go ├── promise_style │ └── promise_style.go ├── download │ └── download.go └── interceptor │ └── interceptor.go ├── .golangci.yml ├── makefile ├── CHANGELOG.md ├── logger.go ├── README.md ├── LICENSE ├── client.go └── axios4go_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rezmoss/axios4go 2 | 3 | go 1.22.5 4 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release-please: 10 | runs-on: 11 | - ubuntu-latest 12 | steps: 13 | - uses: googleapis/release-please-action@v4 14 | id: release-please 15 | with: 16 | release-type: simple # i see no difference between "go" and "simple" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /examples/simple_style/simple_style.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/rezmoss/axios4go" 8 | ) 9 | 10 | func main() { 11 | resp, err := axios4go.Get("https://api.github.com/users/rezmoss") 12 | if err != nil { 13 | log.Fatalf("Error fetching user: %v", err) 14 | } 15 | 16 | var user map[string]interface{} 17 | err = resp.JSON(&user) 18 | if err != nil { 19 | log.Fatalf("Error parsing user JSON: %v", err) 20 | } 21 | 22 | fmt.Printf("GitHub User: %s\n", user["login"]) 23 | fmt.Printf("Name: %s\n", user["name"]) 24 | fmt.Printf("Public Repos: %v\n", user["public_repos"]) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /examples/request_style/request_style.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/rezmoss/axios4go" 8 | ) 9 | 10 | func main() { 11 | resp, err := axios4go.Request("GET", "https://api.github.com/users/rezmoss") 12 | if err != nil { 13 | log.Fatalf("Error fetching user: %v", err) 14 | } 15 | 16 | var user map[string]interface{} 17 | err = resp.JSON(&user) 18 | if err != nil { 19 | log.Fatalf("Error parsing user JSON: %v", err) 20 | } 21 | 22 | fmt.Printf("GitHub User: %s\n", user["login"]) 23 | fmt.Printf("Name: %s\n", user["name"]) 24 | fmt.Printf("Public Repos: %v\n", user["public_repos"]) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /examples/promise_style/promise_style.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rezmoss/axios4go" 7 | ) 8 | 9 | func main() { 10 | axios4go.GetAsync("https://api.github.com/users/rezmoss"). 11 | Then(func(response *axios4go.Response) { 12 | // Handle successful response 13 | var user map[string]interface{} 14 | err := response.JSON(&user) 15 | if err != nil { 16 | fmt.Printf("Error parsing JSON: %v\n", err) 17 | return 18 | } 19 | fmt.Printf("GitHub User: %s\n", user["login"]) 20 | fmt.Printf("Name: %s\n", user["name"]) 21 | fmt.Printf("Public Repos: %v\n", user["public_repos"]) 22 | }). 23 | Catch(func(err error) { 24 | // Handle error 25 | fmt.Printf("Error occurred: %v\n", err) 26 | }). 27 | Finally(func() { 28 | // This will always be executed 29 | fmt.Println("Request completed") 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | modules-download-mode: readonly 4 | linters: 5 | disable-all: true 6 | enable: 7 | - errcheck 8 | - gosimple 9 | - govet 10 | - ineffassign 11 | - revive 12 | - staticcheck 13 | - unused 14 | 15 | linters-settings: 16 | revive: 17 | rules: 18 | - name: blank-imports 19 | - name: context-as-argument 20 | arguments: 21 | - allowTypesBefore: "*testing.T" 22 | - name: context-keys-type 23 | - name: dot-imports 24 | - name: empty-block 25 | - name: error-naming 26 | - name: error-return 27 | - name: error-strings 28 | - name: errorf 29 | - name: increment-decrement 30 | - name: indent-error-flow 31 | - name: range 32 | - name: receiver-naming 33 | - name: redefines-builtin-id 34 | - name: superfluous-else 35 | - name: time-naming 36 | - name: unexported-return 37 | - name: unreachable-code 38 | - name: unused-parameter 39 | - name: var-declaration 40 | - name: var-naming 41 | 42 | issues: 43 | exclude-use-default: false 44 | exclude-files: 45 | - ".*_test.go" 46 | exclude-dirs: 47 | - vendor 48 | - examples 49 | output: 50 | formats: colored-line-number 51 | -------------------------------------------------------------------------------- /.github/workflows/test-fmt.yaml: -------------------------------------------------------------------------------- 1 | name: Go Package CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: '1.22' 19 | 20 | - name: Install dependencies 21 | run: make deps 22 | 23 | - name: Run all checks and tests 24 | run: | 25 | make test-all 26 | make check-examples 27 | 28 | format: 29 | needs: test 30 | runs-on: ubuntu-latest 31 | if: github.event_name == 'pull_request' 32 | steps: 33 | - uses: actions/checkout@v3 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v3 37 | with: 38 | go-version: '1.22' 39 | 40 | - name: Format code 41 | run: make fmt 42 | 43 | - name: Check for changes 44 | id: git-check 45 | run: | 46 | git diff --exit-code || echo "changes=true" >> $GITHUB_OUTPUT 47 | 48 | - name: Create Pull Request 49 | if: steps.git-check.outputs.changes == 'true' 50 | uses: peter-evans/create-pull-request@v5 51 | with: 52 | commit-message: Apply formatting changes 53 | title: 'Auto-format Go code' 54 | body: 'This PR applies automatic formatting to Go code.' 55 | branch: auto-format-${{ github.head_ref }} -------------------------------------------------------------------------------- /examples/download/download.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/rezmoss/axios4go" 9 | ) 10 | 11 | func main() { 12 | url := "https://ash-speed.hetzner.com/1GB.bin" 13 | outputPath := "1GB.bin" 14 | 15 | startTime := time.Now() 16 | lastPrintTime := startTime 17 | 18 | resp, err := axios4go.Get(url, &axios4go.RequestOptions{ 19 | MaxContentLength: 5 * 1024 * 1024 * 1024, // 5GB 20 | Timeout: 60000 * 5, 21 | OnDownloadProgress: func(bytesRead, totalBytes int64) { 22 | currentTime := time.Now() 23 | if currentTime.Sub(lastPrintTime) >= time.Second || bytesRead == totalBytes { 24 | percentage := float64(bytesRead) / float64(totalBytes) * 100 25 | downloadedMB := float64(bytesRead) / 1024 / 1024 26 | totalMB := float64(totalBytes) / 1024 / 1024 27 | elapsedTime := currentTime.Sub(startTime) 28 | speed := float64(bytesRead) / elapsedTime.Seconds() / 1024 / 1024 // MB/s 29 | 30 | fmt.Printf("\rDownloaded %.2f%% (%.2f MB / %.2f MB) - Speed: %.2f MB/s", 31 | percentage, downloadedMB, totalMB, speed) 32 | 33 | lastPrintTime = currentTime 34 | } 35 | }, 36 | }) 37 | 38 | if err != nil { 39 | fmt.Printf("\nError downloading file: %v\n", err) 40 | return 41 | } 42 | 43 | err = writeResponseToFile(resp, outputPath) 44 | if err != nil { 45 | fmt.Printf("\nError writing file: %v\n", err) 46 | return 47 | } 48 | 49 | fmt.Println("\nDownload completed successfully!!") 50 | } 51 | 52 | func writeResponseToFile(resp *axios4go.Response, outputPath string) error { 53 | return os.WriteFile(outputPath, resp.Body, 0644) 54 | } 55 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Makefile for axios4go 2 | 3 | # Go parameters 4 | GOCMD=go 5 | GOBUILD=$(GOCMD) build 6 | GOCLEAN=$(GOCMD) clean 7 | GOTEST=$(GOCMD) test 8 | GOGET=$(GOCMD) get 9 | GOMOD=$(GOCMD) mod 10 | GOFMT=gofmt 11 | GOVET=$(GOCMD) vet 12 | GOCOVER=$(GOCMD) tool cover 13 | 14 | # Binary name 15 | BINARY_NAME=axios4go 16 | 17 | # Test flags 18 | TEST_FLAGS=-v 19 | RACE_FLAGS=-race 20 | COVERAGE_FLAGS=-coverprofile=coverage.out 21 | 22 | all: test build 23 | 24 | build: 25 | $(GOBUILD) -o $(BINARY_NAME) -v 26 | 27 | test: 28 | CGO_ENABLED=0 $(GOTEST) $(TEST_FLAGS) $(shell go list ./... | grep -v /examples) 29 | 30 | test-race: 31 | $(GOTEST) $(TEST_FLAGS) $(RACE_FLAGS) $(shell go list ./... | grep -v /examples) 32 | 33 | test-coverage: 34 | CGO_ENABLED=0 $(GOTEST) $(TEST_FLAGS) $(COVERAGE_FLAGS) $(shell go list ./... | grep -v /examples) 35 | $(GOCOVER) -func=coverage.out 36 | 37 | test-all: test test-race test-coverage 38 | 39 | benchmark: 40 | $(GOTEST) -run=^$$ -bench=. -benchmem $(shell go list ./... | grep -v /examples) 41 | 42 | clean: 43 | $(GOCLEAN) 44 | rm -f $(BINARY_NAME) coverage.out 45 | 46 | run: 47 | $(GOBUILD) -o $(BINARY_NAME) -v 48 | ./$(BINARY_NAME) 49 | 50 | deps: 51 | $(GOGET) -v -t -d ./... 52 | $(GOMOD) tidy 53 | 54 | # Format all Go files 55 | fmt: 56 | $(GOFMT) -s -w . 57 | 58 | # Run go vet 59 | vet: 60 | $(GOVET) ./... 61 | 62 | # Run gocyclo 63 | cyclo: 64 | @which gocyclo > /dev/null || go install github.com/fzipp/gocyclo/cmd/gocyclo@latest 65 | gocyclo -over 15 . 66 | 67 | # Check examples 68 | check-examples: 69 | @echo "Checking Go files in examples/ ..." 70 | @find examples -type f -name '*.go' | while read file; do \ 71 | echo " Checking $$file"; \ 72 | $(GOCMD) build -o /dev/null "$$file" || exit 1; \ 73 | done 74 | 75 | # Run all checks and tests 76 | check: fmt vet cyclo test-all check-examples 77 | 78 | # Install gocyclo if not present 79 | install-gocyclo: 80 | @which gocyclo > /dev/null || go install github.com/fzipp/gocyclo/cmd/gocyclo@latest 81 | 82 | .PHONY: all build test test-race test-coverage test-all benchmark clean run deps fmt vet cyclo check-examples check install-gocyclo -------------------------------------------------------------------------------- /examples/interceptor/interceptor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/rezmoss/axios4go" 9 | ) 10 | 11 | func main() { 12 | // Define request interceptor 13 | requestInterceptor := func(req *http.Request) error { 14 | req.Header.Set("User-Agent", "axios4go-example") 15 | fmt.Println("Request interceptor: Added User-Agent header") 16 | return nil 17 | } 18 | 19 | // Define response interceptor 20 | responseInterceptor := func(resp *http.Response) error { 21 | fmt.Printf("Response interceptor: Status Code %d\n", resp.StatusCode) 22 | return nil 23 | } 24 | 25 | // Create a new client 26 | client := axios4go.NewClient("https://api.github.com") 27 | 28 | // Create custom transport with interceptors 29 | transport := &InterceptorTransport{ 30 | Base: http.DefaultTransport, 31 | RequestInterceptor: requestInterceptor, 32 | ResponseInterceptor: responseInterceptor, 33 | } 34 | 35 | // Set the custom transport to the client's HTTP client 36 | client.HTTPClient.Transport = transport 37 | 38 | // Send the request 39 | response, err := client.Request(&axios4go.RequestOptions{}) 40 | if err != nil { 41 | log.Fatalf("Request failed: %v", err) 42 | } 43 | 44 | // Print the response status and body 45 | fmt.Printf("Response Status: %d\n", response.StatusCode) 46 | fmt.Printf("Response Body: %s\n", string(response.Body)) 47 | } 48 | 49 | // InterceptorTransport is a custom http.RoundTripper that applies interceptors 50 | type InterceptorTransport struct { 51 | Base http.RoundTripper 52 | RequestInterceptor func(*http.Request) error 53 | ResponseInterceptor func(*http.Response) error 54 | } 55 | 56 | func (t *InterceptorTransport) RoundTrip(req *http.Request) (*http.Response, error) { 57 | // Apply request interceptor 58 | if t.RequestInterceptor != nil { 59 | if err := t.RequestInterceptor(req); err != nil { 60 | return nil, err 61 | } 62 | } 63 | 64 | // Perform the actual request 65 | resp, err := t.Base.RoundTrip(req) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | // Apply response interceptor 71 | if t.ResponseInterceptor != nil { 72 | if err := t.ResponseInterceptor(resp); err != nil { 73 | return nil, err 74 | } 75 | } 76 | 77 | return resp, nil 78 | } 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.6.3](https://github.com/rezmoss/axios4go/compare/v0.6.2...v0.6.3) (2025-10-06) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * prevent memory exhaustion in response size check ([c3f98aa](https://github.com/rezmoss/axios4go/commit/c3f98aa064ef67e651e7126265bcb7f2952952bd)) 9 | * prevent memory exhaustion in response size check ([04f4cac](https://github.com/rezmoss/axios4go/commit/04f4cacc7b7b3e70ba9bcf9527d4f35afa829604)) 10 | 11 | ## [0.6.2](https://github.com/rezmoss/axios4go/compare/v0.6.1...v0.6.2) (2025-01-22) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * cover more tests, fixed [#4](https://github.com/rezmoss/axios4go/issues/4) ([26ef773](https://github.com/rezmoss/axios4go/commit/26ef7730714130b31a89a9cc37c62837d11367b6)) 17 | 18 | ## [0.6.1](https://github.com/rezmoss/axios4go/compare/v0.6.0...v0.6.1) (2025-01-21) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * respect Go initialism and acronyms ([6105a93](https://github.com/rezmoss/axios4go/commit/6105a9344165089baec3b8264331965bf07113e7)) 24 | 25 | ## [0.6.0](https://github.com/rezmoss/axios4go/compare/v0.5.0...v0.6.0) (2025-01-11) 26 | 27 | 28 | ### Features 29 | 30 | * Logger ([bd783eb](https://github.com/rezmoss/axios4go/commit/bd783eb52b891783271b0920984d1c52f8d280e5)) 31 | 32 | ## [0.5.0](https://github.com/rezmoss/axios4go/compare/v0.4.0...v0.5.0) (2024-10-10) 33 | 34 | 35 | ### Features 36 | 37 | * Progress Tracking for Uploads/Downloads ([426ffaf](https://github.com/rezmoss/axios4go/commit/426ffaf0edaf33b8bbf3fae455bf1f9428ef12c8)) 38 | * Progress Tracking for Uploads/Downloads , fixed [#22](https://github.com/rezmoss/axios4go/issues/22) ([9324508](https://github.com/rezmoss/axios4go/commit/9324508733a342748cfdbfe02f9c99e3ffaba166)) 39 | 40 | ## [0.4.0](https://github.com/rezmoss/axios4go/compare/v0.3.0...v0.4.0) (2024-10-10) 41 | 42 | 43 | ### Features 44 | 45 | * support defines proxy setting for request ([730c775](https://github.com/rezmoss/axios4go/commit/730c775d8c1057cfc7b064e3de28ef89c13cd3e2)) 46 | 47 | ## [0.3.0](https://github.com/rezmoss/axios4go/compare/v0.2.1...v0.3.0) (2024-10-08) 48 | 49 | 50 | ### Features 51 | 52 | * add request and response interceptors to client requests ([a1ecc99](https://github.com/rezmoss/axios4go/commit/a1ecc998aa96eaf32404204be4d36f8f9f01d26a)) 53 | 54 | ## [0.2.1](https://github.com/rezmoss/axios4go/compare/v0.2.0...v0.2.1) (2024-09-29) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * update doc ([1c9e74d](https://github.com/rezmoss/axios4go/commit/1c9e74d1db623fa0acfffa168f268665bf539516)) 60 | 61 | ## [0.2.0](https://github.com/rezmoss/axios4go/compare/v0.1.0...v0.2.0) (2024-09-19) 62 | 63 | 64 | ### Miscellaneous Chores 65 | 66 | * release 0.2.0 ([f5af040](https://github.com/rezmoss/axios4go/commit/f5af040ab3a15fff104d8154f79f59bcdc46292a)) 67 | 68 | ## 0.2.0 (2024-09-13) 69 | 70 | 71 | ### Miscellaneous Chores 72 | 73 | * release 0.2.0 ([f5af040](https://github.com/Blackvote/axios4go/commit/f5af040ab3a15fff104d8154f79f59bcdc46292a)) 74 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package axios4go 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type LogLevel int 14 | 15 | const ( 16 | LevelNone LogLevel = iota 17 | LevelError 18 | LevelInfo 19 | LevelDebug 20 | ) 21 | 22 | type Logger interface { 23 | LogRequest(*http.Request, LogLevel) 24 | LogResponse(*http.Response, []byte, time.Duration, LogLevel) 25 | LogError(error, LogLevel) 26 | SetLevel(LogLevel) 27 | } 28 | 29 | type LogOptions struct { 30 | Level LogLevel 31 | MaxBodyLength int 32 | MaskHeaders []string 33 | Output io.Writer 34 | TimeFormat string 35 | IncludeBody bool 36 | IncludeHeaders bool 37 | } 38 | 39 | type DefaultLogger struct { 40 | options LogOptions 41 | } 42 | 43 | func NewDefaultLogger(options LogOptions) *DefaultLogger { 44 | if options.Output == nil { 45 | options.Output = os.Stdout 46 | } 47 | if options.TimeFormat == "" { 48 | options.TimeFormat = time.RFC3339 49 | } 50 | if options.MaxBodyLength == 0 { 51 | options.MaxBodyLength = 1000 52 | } 53 | return &DefaultLogger{options: options} 54 | } 55 | 56 | func (l *DefaultLogger) SetLevel(level LogLevel) { 57 | l.options.Level = level 58 | } 59 | 60 | func (l *DefaultLogger) LogRequest(req *http.Request, level LogLevel) { 61 | if level > l.options.Level { 62 | return 63 | } 64 | 65 | var buf strings.Builder 66 | timestamp := time.Now().Format(l.options.TimeFormat) 67 | 68 | fmt.Fprintf(&buf, "[%s] REQUEST: %s %s\n", timestamp, req.Method, req.URL) 69 | 70 | if l.options.IncludeHeaders { 71 | buf.WriteString("Headers:\n") 72 | for key, vals := range req.Header { 73 | if l.isHeaderMasked(key) { 74 | fmt.Fprintf(&buf, " %s: [MASKED]\n", key) 75 | } else { 76 | fmt.Fprintf(&buf, " %s: %s\n", key, strings.Join(vals, ", ")) 77 | } 78 | } 79 | } 80 | 81 | if l.options.IncludeBody && req.Body != nil { 82 | body, err := io.ReadAll(req.Body) 83 | if err == nil { 84 | req.Body = io.NopCloser(bytes.NewBuffer(body)) 85 | if len(body) > l.options.MaxBodyLength { 86 | fmt.Fprintf(&buf, "Body: (truncated) %s...\n", body[:l.options.MaxBodyLength]) 87 | } else { 88 | fmt.Fprintf(&buf, "Body: %s\n", body) 89 | } 90 | } 91 | } 92 | 93 | fmt.Fprintln(l.options.Output, buf.String()) 94 | } 95 | 96 | func (l *DefaultLogger) LogResponse(resp *http.Response, body []byte, duration time.Duration, level LogLevel) { 97 | if level > l.options.Level { 98 | return 99 | } 100 | 101 | var buf strings.Builder 102 | timestamp := time.Now().Format(l.options.TimeFormat) 103 | 104 | fmt.Fprintf(&buf, "[%s] RESPONSE: %d %s (%.2fms)\n", 105 | timestamp, resp.StatusCode, resp.Status, float64(duration.Microseconds())/1000) 106 | 107 | if l.options.IncludeHeaders { 108 | buf.WriteString("Headers:\n") 109 | for key, vals := range resp.Header { 110 | if l.isHeaderMasked(key) { 111 | fmt.Fprintf(&buf, " %s: [MASKED]\n", key) 112 | } else { 113 | fmt.Fprintf(&buf, " %s: %s\n", key, strings.Join(vals, ", ")) 114 | } 115 | } 116 | } 117 | 118 | if l.options.IncludeBody && body != nil { 119 | if len(body) > l.options.MaxBodyLength { 120 | fmt.Fprintf(&buf, "Body: (truncated) %s...\n", body[:l.options.MaxBodyLength]) 121 | } else { 122 | fmt.Fprintf(&buf, "Body: %s\n", body) 123 | } 124 | } 125 | 126 | fmt.Fprintln(l.options.Output, buf.String()) 127 | } 128 | 129 | func (l *DefaultLogger) LogError(err error, level LogLevel) { 130 | if level > l.options.Level { 131 | return 132 | } 133 | 134 | timestamp := time.Now().Format(l.options.TimeFormat) 135 | fmt.Fprintf(l.options.Output, "[%s] ERROR: %v\n", timestamp, err) 136 | } 137 | 138 | func (l *DefaultLogger) isHeaderMasked(header string) bool { 139 | header = strings.ToLower(header) 140 | for _, masked := range l.options.MaskHeaders { 141 | if strings.ToLower(masked) == header { 142 | return true 143 | } 144 | } 145 | return false 146 | } 147 | 148 | func NewLogger(level LogLevel) Logger { 149 | return NewDefaultLogger(LogOptions{ 150 | Level: level, 151 | MaxBodyLength: 1000, 152 | MaskHeaders: []string{"Authorization", "Cookie", "Set-Cookie"}, 153 | Output: os.Stdout, 154 | TimeFormat: time.RFC3339, 155 | IncludeBody: true, 156 | IncludeHeaders: true, 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axios4go 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/rezmoss/axios4go.svg)](https://pkg.go.dev/github.com/rezmoss/axios4go) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/rezmoss/axios4go)](https://goreportcard.com/report/github.com/rezmoss/axios4go) 5 | [![Release](https://img.shields.io/github/v/release/rezmoss/axios4go.svg?style=flat-square)](https://github.com/rezmoss/axios4go/releases) 6 | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9401/badge)](https://www.bestpractices.dev/projects/9401) 7 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 8 | 9 | axios4go is a Go HTTP client library inspired by [Axios](https://github.com/axios/axios) (a promise based HTTP client for node.js), providing a simple and intuitive API for making HTTP requests. It offers features like JSON handling, configurable instances, and support for various HTTP methods. 10 | 11 | ## Table of Contents 12 | 13 | - [Features](#features) 14 | - [Installation](#installation) 15 | - [Usage](#usage) 16 | - [Making a Simple Request](#making-a-simple-request) 17 | - [Using Request Options](#using-request-options) 18 | - [Making POST Requests](#making-post-requests) 19 | - [Using Async Requests](#using-async-requests) 20 | - [Creating a Custom Client](#creating-a-custom-client) 21 | - [Using Interceptors](#using-interceptors) 22 | - [Handling Progress](#handling-progress) 23 | - [Using Proxy](#using-proxy) 24 | - [Configuration Options](#configuration-options) 25 | - [Contributing](#contributing) 26 | - [License](#license) 27 | 28 | ## Features 29 | 30 | - Simple and intuitive API 31 | - Support for `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`, and `PATCH` methods 32 | - JSON request and response handling 33 | - Configurable client instances 34 | - Global and per-request timeout management 35 | - Redirect management 36 | - Basic authentication support 37 | - Customizable request options 38 | - Promise-like asynchronous requests 39 | - Request and response interceptors 40 | - Upload and download progress tracking 41 | - Proxy support 42 | - Configurable max content length and body length 43 | 44 | ## Installation 45 | 46 | To install `axios4go`, use `go get`: 47 | 48 | ```bash 49 | go get -u github.com/rezmoss/axios4go 50 | ``` 51 | 52 | **Note**: Requires Go 1.13 or later. 53 | 54 | ## Usage 55 | 56 | ### Making a Simple Request 57 | 58 | ```go 59 | package main 60 | 61 | import ( 62 | "fmt" 63 | 64 | "github.com/rezmoss/axios4go" 65 | ) 66 | 67 | func main() { 68 | resp, err := axios4go.Get("https://api.example.com/data") 69 | if err != nil { 70 | fmt.Printf("Error: %v\n", err) 71 | return 72 | } 73 | fmt.Printf("Status Code: %d\n", resp.StatusCode) 74 | fmt.Printf("Body: %s\n", string(resp.Body)) 75 | } 76 | ``` 77 | 78 | ### Using Request Options 79 | 80 | ```go 81 | resp, err := axios4go.Get("https://api.example.com/data", &axios4go.RequestOptions{ 82 | Headers: map[string]string{ 83 | "Authorization": "Bearer token", 84 | }, 85 | Params: map[string]string{ 86 | "query": "golang", 87 | }, 88 | }) 89 | ``` 90 | 91 | ### Making POST Requests 92 | 93 | ```go 94 | body := map[string]interface{}{ 95 | "name": "John Doe", 96 | "age": 30, 97 | } 98 | resp, err := axios4go.Post("https://api.example.com/users", body) 99 | if err != nil { 100 | fmt.Printf("Error: %v\n", err) 101 | return 102 | } 103 | fmt.Printf("Status Code: %d\n", resp.StatusCode) 104 | fmt.Printf("Body: %s\n", string(resp.Body)) 105 | ``` 106 | 107 | ### Using Async Requests 108 | 109 | ```go 110 | axios4go.GetAsync("https://api.example.com/data"). 111 | Then(func(response *axios4go.Response) { 112 | fmt.Printf("Status Code: %d\n", response.StatusCode) 113 | fmt.Printf("Body: %s\n", string(response.Body)) 114 | }). 115 | Catch(func(err error) { 116 | fmt.Printf("Error: %v\n", err) 117 | }). 118 | Finally(func() { 119 | fmt.Println("Request completed") 120 | }) 121 | ``` 122 | 123 | ### Creating a Custom Client 124 | 125 | ```go 126 | client := axios4go.NewClient("https://api.example.com") 127 | 128 | resp, err := client.Request(&axios4go.RequestOptions{ 129 | Method: "GET", 130 | URL: "/users", 131 | Headers: map[string]string{ 132 | "Authorization": "Bearer token", 133 | }, 134 | }) 135 | if err != nil { 136 | fmt.Printf("Error: %v\n", err) 137 | return 138 | } 139 | fmt.Printf("Status Code: %d\n", resp.StatusCode) 140 | fmt.Printf("Body: %s\n", string(resp.Body)) 141 | ``` 142 | 143 | ### Using Interceptors 144 | 145 | ```go 146 | options := &axios4go.RequestOptions{ 147 | InterceptorOptions: axios4go.InterceptorOptions{ 148 | RequestInterceptors: []func(*http.Request) error{ 149 | func(req *http.Request) error { 150 | req.Header.Set("X-Custom-Header", "value") 151 | return nil 152 | }, 153 | }, 154 | ResponseInterceptors: []func(*http.Response) error{ 155 | func(resp *http.Response) error { 156 | fmt.Printf("Response received with status: %d\n", resp.StatusCode) 157 | return nil 158 | }, 159 | }, 160 | }, 161 | } 162 | 163 | resp, err := axios4go.Get("https://api.example.com/data", options) 164 | ``` 165 | 166 | ### Handling Progress 167 | 168 | ```go 169 | options := &axios4go.RequestOptions{ 170 | OnUploadProgress: func(bytesRead, totalBytes int64) { 171 | fmt.Printf("Upload progress: %d/%d bytes\n", bytesRead, totalBytes) 172 | }, 173 | OnDownloadProgress: func(bytesRead, totalBytes int64) { 174 | fmt.Printf("Download progress: %d/%d bytes\n", bytesRead, totalBytes) 175 | }, 176 | } 177 | 178 | resp, err := axios4go.Post("https://api.example.com/upload", largeData, options) 179 | ``` 180 | 181 | ### Using Proxy 182 | 183 | ```go 184 | options := &axios4go.RequestOptions{ 185 | Proxy: &axios4go.Proxy{ 186 | Protocol: "http", 187 | Host: "proxy.example.com", 188 | Port: 8080, 189 | Auth: &axios4go.Auth{ 190 | Username: "proxyuser", 191 | Password: "proxypass", 192 | }, 193 | }, 194 | } 195 | 196 | resp, err := axios4go.Get("https://api.example.com/data", options) 197 | ``` 198 | 199 | ## Configuration Options 200 | 201 | `axios4go` supports various configuration options through the `RequestOptions` struct: 202 | 203 | - **Method**: HTTP method (`GET`, `POST`, etc.) 204 | - **URL**: Request URL (relative to `BaseURL` if provided) 205 | - **BaseURL**: Base URL for the request (overrides client's `BaseURL` if set) 206 | - **Params**: URL query parameters (`map[string]string`) 207 | - **Body**: Request body (can be `string`, `[]byte`, or any JSON serializable object) 208 | - **Headers**: Custom headers (`map[string]string`) 209 | - **Timeout**: Request timeout in milliseconds 210 | - **Auth**: Basic authentication credentials (`&Auth{Username: "user", Password: "pass"}`) 211 | - **ResponseType**: Expected response type (default is "json") 212 | - **ResponseEncoding**: Expected response encoding (default is "utf8") 213 | - **MaxRedirects**: Maximum number of redirects to follow 214 | - **MaxContentLength**: Maximum allowed response content length 215 | - **MaxBodyLength**: Maximum allowed request body length 216 | - **Decompress**: Whether to decompress the response body (default is true) 217 | - **ValidateStatus**: Function to validate HTTP response status codes 218 | - **InterceptorOptions**: Request and response interceptors 219 | - **Proxy**: Proxy configuration 220 | - **OnUploadProgress**: Function to track upload progress 221 | - **OnDownloadProgress**: Function to track download progress 222 | 223 | **Example**: 224 | 225 | ```go 226 | options := &axios4go.RequestOptions{ 227 | Method: "POST", 228 | URL: "/submit", 229 | Headers: map[string]string{ 230 | "Content-Type": "application/json", 231 | }, 232 | Body: map[string]interface{}{ 233 | "title": "Sample", 234 | "content": "This is a sample post.", 235 | }, 236 | Auth: &axios4go.Auth{ 237 | Username: "user", 238 | Password: "pass", 239 | }, 240 | Params: map[string]string{ 241 | "verbose": "true", 242 | }, 243 | Timeout: 5000, // 5 seconds 244 | MaxRedirects: 5, 245 | MaxContentLength: 1024 * 1024, // 1MB 246 | ValidateStatus: func(statusCode int) bool { 247 | return statusCode >= 200 && statusCode < 300 248 | }, 249 | } 250 | 251 | resp, err := client.Request(options) 252 | ``` 253 | 254 | ## Contributing 255 | 256 | Contributions to `axios4go` are welcome! Please follow these guidelines: 257 | 258 | - **Fork the repository** and create a new branch for your feature or bug fix. 259 | - **Ensure your code follows Go conventions** and passes all tests. 260 | - **Write tests** for new features or bug fixes. 261 | - **Submit a Pull Request** with a clear description of your changes. 262 | 263 | ## License 264 | 265 | This project is licensed under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for details -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package axios4go 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | type Client struct { 18 | BaseURL string 19 | HTTPClient *http.Client 20 | Logger Logger 21 | } 22 | 23 | type Response struct { 24 | StatusCode int 25 | Headers http.Header 26 | Body []byte 27 | } 28 | 29 | type Promise struct { 30 | response *Response 31 | err error 32 | then func(*Response) 33 | catch func(error) 34 | finally func() 35 | done chan struct{} 36 | mu sync.Mutex 37 | } 38 | 39 | type RequestInterceptors []func(*http.Request) error 40 | type ResponseInterceptors []func(*http.Response) error 41 | type InterceptorOptions struct { 42 | RequestInterceptors RequestInterceptors 43 | ResponseInterceptors ResponseInterceptors 44 | } 45 | 46 | type RequestOptions struct { 47 | Method string 48 | URL string 49 | BaseURL string 50 | Params map[string]string 51 | Body interface{} 52 | Headers map[string]string 53 | Timeout int 54 | Auth *Auth 55 | ResponseType string 56 | ResponseEncoding string 57 | MaxRedirects int 58 | MaxContentLength int64 59 | MaxBodyLength int64 60 | Decompress bool 61 | ValidateStatus func(int) bool 62 | InterceptorOptions InterceptorOptions 63 | Proxy *Proxy 64 | OnUploadProgress func(bytesRead, totalBytes int64) 65 | OnDownloadProgress func(bytesRead, totalBytes int64) 66 | LogLevel LogLevel 67 | } 68 | 69 | type Proxy struct { 70 | Protocol string 71 | Host string 72 | Port int 73 | Auth *Auth 74 | } 75 | 76 | type Auth struct { 77 | Username string 78 | Password string 79 | } 80 | 81 | type ProgressReader struct { 82 | reader io.Reader 83 | total int64 84 | read int64 85 | onProgress func(bytesRead, totalBytes int64) 86 | } 87 | 88 | type ProgressWriter struct { 89 | writer io.Writer 90 | total int64 91 | written int64 92 | onProgress func(bytesWritten, totalBytes int64) 93 | } 94 | 95 | func (pr *ProgressReader) Read(p []byte) (int, error) { 96 | n, err := pr.reader.Read(p) 97 | pr.read += int64(n) 98 | if pr.onProgress != nil { 99 | pr.onProgress(pr.read, pr.total) 100 | } 101 | return n, err 102 | } 103 | 104 | func (pw *ProgressWriter) Write(p []byte) (int, error) { 105 | n, err := pw.writer.Write(p) 106 | pw.written += int64(n) 107 | if pw.onProgress != nil { 108 | pw.onProgress(pw.written, pw.total) 109 | } 110 | return n, err 111 | } 112 | 113 | var defaultClient = &Client{HTTPClient: &http.Client{}, Logger: NewLogger(LevelNone)} 114 | 115 | func (r *Response) JSON(v interface{}) error { 116 | return json.Unmarshal(r.Body, v) 117 | } 118 | 119 | func (p *Promise) Then(fn func(*Response)) *Promise { 120 | p.mu.Lock() 121 | defer p.mu.Unlock() 122 | 123 | if p.response != nil && p.err == nil { 124 | fn(p.response) 125 | } else { 126 | p.then = fn 127 | } 128 | return p 129 | } 130 | 131 | func (p *Promise) Catch(fn func(error)) *Promise { 132 | p.mu.Lock() 133 | defer p.mu.Unlock() 134 | 135 | if p.err != nil { 136 | fn(p.err) 137 | } else { 138 | p.catch = fn 139 | } 140 | return p 141 | } 142 | 143 | func (p *Promise) Finally(fn func()) { 144 | p.mu.Lock() 145 | 146 | if p.response != nil || p.err != nil { 147 | p.mu.Unlock() 148 | fn() 149 | } else { 150 | p.finally = fn 151 | p.mu.Unlock() 152 | } 153 | 154 | <-p.done 155 | } 156 | 157 | func NewPromise() *Promise { 158 | return &Promise{ 159 | done: make(chan struct{}), 160 | } 161 | } 162 | 163 | func (p *Promise) resolve(resp *Response, err error) { 164 | p.mu.Lock() 165 | defer p.mu.Unlock() 166 | 167 | p.response = resp 168 | p.err = err 169 | 170 | if p.then != nil && err == nil { 171 | p.then(resp) 172 | } 173 | if p.catch != nil && err != nil { 174 | p.catch(err) 175 | } 176 | if p.finally != nil { 177 | p.finally() 178 | } 179 | 180 | close(p.done) 181 | } 182 | 183 | func Get(urlStr string, options ...*RequestOptions) (*Response, error) { 184 | return Request("GET", urlStr, options...) 185 | } 186 | 187 | func GetAsync(urlStr string, options ...*RequestOptions) *Promise { 188 | promise := NewPromise() 189 | 190 | go func() { 191 | resp, err := Request("GET", urlStr, options...) 192 | promise.resolve(resp, err) 193 | }() 194 | 195 | return promise 196 | } 197 | 198 | func Post(urlStr string, body interface{}, options ...*RequestOptions) (*Response, error) { 199 | mergedOptions := mergeBodyIntoOptions(body, options) 200 | return Request("POST", urlStr, mergedOptions) 201 | } 202 | 203 | func PostAsync(urlStr string, body interface{}, options ...*RequestOptions) *Promise { 204 | mergedOptions := mergeBodyIntoOptions(body, options) 205 | promise := NewPromise() 206 | 207 | go func() { 208 | resp, err := Request("POST", urlStr, mergedOptions) 209 | promise.resolve(resp, err) 210 | }() 211 | 212 | return promise 213 | } 214 | 215 | func mergeBodyIntoOptions(body interface{}, options []*RequestOptions) *RequestOptions { 216 | mergedOption := &RequestOptions{ 217 | Body: body, 218 | } 219 | 220 | if len(options) > 0 { 221 | *mergedOption = *options[0] 222 | mergedOption.Body = body 223 | } 224 | 225 | return mergedOption 226 | } 227 | 228 | func Put(urlStr string, body interface{}, options ...*RequestOptions) (*Response, error) { 229 | mergedOptions := mergeBodyIntoOptions(body, options) 230 | return Request("PUT", urlStr, mergedOptions) 231 | } 232 | 233 | func PutAsync(urlStr string, body interface{}, options ...*RequestOptions) *Promise { 234 | mergedOptions := mergeBodyIntoOptions(body, options) 235 | promise := NewPromise() 236 | 237 | go func() { 238 | resp, err := Request("PUT", urlStr, mergedOptions) 239 | promise.resolve(resp, err) 240 | }() 241 | 242 | return promise 243 | } 244 | 245 | func Delete(urlStr string, options ...*RequestOptions) (*Response, error) { 246 | return Request("DELETE", urlStr, options...) 247 | } 248 | 249 | func DeleteAsync(urlStr string, options ...*RequestOptions) *Promise { 250 | promise := NewPromise() 251 | go func() { 252 | resp, err := Request("DELETE", urlStr, options...) 253 | promise.resolve(resp, err) 254 | }() 255 | return promise 256 | } 257 | 258 | func Head(urlStr string, options ...*RequestOptions) (*Response, error) { 259 | return Request("HEAD", urlStr, options...) 260 | } 261 | 262 | func HeadAsync(urlStr string, options ...*RequestOptions) *Promise { 263 | promise := NewPromise() 264 | go func() { 265 | resp, err := Request("HEAD", urlStr, options...) 266 | promise.resolve(resp, err) 267 | }() 268 | return promise 269 | } 270 | 271 | func Options(urlStr string, options ...*RequestOptions) (*Response, error) { 272 | return Request("OPTIONS", urlStr, options...) 273 | } 274 | 275 | func OptionsAsync(urlStr string, options ...*RequestOptions) *Promise { 276 | promise := NewPromise() 277 | go func() { 278 | resp, err := Request("OPTIONS", urlStr, options...) 279 | promise.resolve(resp, err) 280 | }() 281 | return promise 282 | } 283 | 284 | func Patch(urlStr string, body interface{}, options ...*RequestOptions) (*Response, error) { 285 | mergedOptions := mergeBodyIntoOptions(body, options) 286 | return Request("PATCH", urlStr, mergedOptions) 287 | } 288 | 289 | func PatchAsync(urlStr string, body interface{}, options ...*RequestOptions) *Promise { 290 | mergedOptions := mergeBodyIntoOptions(body, options) 291 | promise := NewPromise() 292 | 293 | go func() { 294 | resp, err := Request("PATCH", urlStr, mergedOptions) 295 | promise.resolve(resp, err) 296 | }() 297 | 298 | return promise 299 | } 300 | 301 | func Request(method, urlStr string, options ...*RequestOptions) (*Response, error) { 302 | reqOptions := &RequestOptions{ 303 | Method: "GET", 304 | URL: urlStr, 305 | Timeout: 1000, 306 | ResponseType: "json", 307 | ResponseEncoding: "utf8", 308 | MaxContentLength: 2000, 309 | MaxBodyLength: 2000, 310 | MaxRedirects: 21, 311 | Decompress: true, 312 | ValidateStatus: nil, 313 | } 314 | 315 | if len(options) > 0 && options[0] != nil { 316 | mergeOptions(reqOptions, options[0]) 317 | } 318 | 319 | if method != "" { 320 | reqOptions.Method = method 321 | } 322 | 323 | return defaultClient.Request(reqOptions) 324 | } 325 | 326 | func RequestAsync(method, urlStr string, options ...*RequestOptions) *Promise { 327 | resp, err := Request(method, urlStr, options...) 328 | return &Promise{response: resp, err: err} 329 | } 330 | 331 | func (c *Client) Request(options *RequestOptions) (*Response, error) { 332 | if options.Timeout == 0 { 333 | options.Timeout = 1000 334 | } 335 | if options.MaxContentLength == 0 { 336 | options.MaxContentLength = 2000 337 | } 338 | if options.MaxBodyLength == 0 { 339 | options.MaxBodyLength = 2000 340 | } 341 | if options.ResponseType == "" { 342 | options.ResponseType = "json" 343 | } 344 | if options.ResponseEncoding == "" { 345 | options.ResponseEncoding = "utf8" 346 | } 347 | if options.MaxRedirects == 0 { 348 | options.MaxRedirects = 21 349 | } 350 | if options.Method == "" { 351 | options.Method = "GET" 352 | } 353 | if !options.Decompress { 354 | options.Decompress = true 355 | } 356 | 357 | validMethods := map[string]bool{ 358 | "GET": true, 359 | "POST": true, 360 | "PUT": true, 361 | "DELETE": true, 362 | "PATCH": true, 363 | "HEAD": true, 364 | "OPTIONS": true, 365 | } 366 | upperMethod := strings.ToUpper(options.Method) 367 | if !validMethods[upperMethod] { 368 | return nil, fmt.Errorf("invalid HTTP method: %q", options.Method) 369 | } 370 | 371 | startTime := time.Now() 372 | var fullURL string 373 | if c.BaseURL != "" { 374 | var err error 375 | fullURL, err = url.JoinPath(c.BaseURL, options.URL) 376 | if err != nil { 377 | return nil, err 378 | } 379 | } else if options.BaseURL != "" { 380 | var err error 381 | fullURL, err = url.JoinPath(options.BaseURL, options.URL) 382 | if err != nil { 383 | return nil, err 384 | } 385 | } else { 386 | fullURL = options.URL 387 | } 388 | 389 | if len(options.Params) > 0 { 390 | parsedURL, err := url.Parse(fullURL) 391 | if err != nil { 392 | return nil, err 393 | } 394 | q := parsedURL.Query() 395 | for k, v := range options.Params { 396 | q.Add(k, v) 397 | } 398 | parsedURL.RawQuery = q.Encode() 399 | fullURL = parsedURL.String() 400 | } 401 | 402 | var bodyReader io.Reader 403 | var bodyLength int64 404 | 405 | if options.Body != nil { 406 | switch v := options.Body.(type) { 407 | case string: 408 | bodyReader = strings.NewReader(v) 409 | bodyLength = int64(len(v)) 410 | case []byte: 411 | bodyReader = bytes.NewReader(v) 412 | bodyLength = int64(len(v)) 413 | default: 414 | jsonBody, err := json.Marshal(options.Body) 415 | if err != nil { 416 | return nil, err 417 | } 418 | bodyReader = bytes.NewBuffer(jsonBody) 419 | bodyLength = int64(len(jsonBody)) 420 | } 421 | if options.MaxBodyLength > 0 && bodyLength > int64(options.MaxBodyLength) { 422 | return nil, errors.New("request body length exceeded maxBodyLength") 423 | } 424 | 425 | if options.Body != nil && options.OnUploadProgress != nil { 426 | bodyReader = &ProgressReader{ 427 | reader: bodyReader, 428 | total: bodyLength, 429 | onProgress: options.OnUploadProgress, 430 | } 431 | } 432 | } 433 | 434 | req, err := http.NewRequest(options.Method, fullURL, bodyReader) 435 | if err != nil { 436 | return nil, err 437 | } 438 | 439 | for _, interceptor := range options.InterceptorOptions.RequestInterceptors { 440 | err = interceptor(req) 441 | if err != nil { 442 | return nil, fmt.Errorf("request interceptor failed: %w", err) 443 | } 444 | } 445 | 446 | if options.Headers == nil { 447 | options.Headers = make(map[string]string) 448 | } 449 | 450 | if options.Body != nil { 451 | if _, exists := options.Headers["Content-Type"]; !exists { 452 | options.Headers["Content-Type"] = "application/json" 453 | } 454 | } 455 | 456 | for key, value := range options.Headers { 457 | req.Header.Set(key, value) 458 | } 459 | 460 | if options.Auth != nil { 461 | auth := options.Auth.Username + ":" + options.Auth.Password 462 | basicAuth := base64.StdEncoding.EncodeToString([]byte(auth)) 463 | req.Header.Set("Authorization", "Basic "+basicAuth) 464 | } 465 | 466 | if c.Logger != nil { 467 | c.Logger.LogRequest(req, options.LogLevel) 468 | } 469 | 470 | c.HTTPClient.Timeout = time.Duration(options.Timeout) * time.Millisecond 471 | 472 | if options.MaxRedirects > 0 { 473 | c.HTTPClient.CheckRedirect = func(_ *http.Request, via []*http.Request) error { 474 | if len(via) >= options.MaxRedirects { 475 | return fmt.Errorf("too many redirects (max: %d)", options.MaxRedirects) 476 | } 477 | return nil 478 | } 479 | } 480 | 481 | if options.Proxy != nil { 482 | proxyStr := fmt.Sprintf("%s://%s:%d", options.Proxy.Protocol, options.Proxy.Host, options.Proxy.Port) 483 | proxyURL, err := url.Parse(proxyStr) 484 | if err != nil { 485 | return nil, err 486 | } 487 | transport := &http.Transport{ 488 | Proxy: http.ProxyURL(proxyURL), 489 | } 490 | if options.Proxy.Auth != nil { 491 | auth := options.Proxy.Auth.Username + ":" + options.Proxy.Auth.Password 492 | basicAuth := base64.StdEncoding.EncodeToString([]byte(auth)) 493 | transport.ProxyConnectHeader = http.Header{ 494 | "Proxy-Authorization": {"Basic " + basicAuth}, 495 | } 496 | } 497 | c.HTTPClient.Transport = transport 498 | defer func() { 499 | c.HTTPClient.Transport = nil 500 | }() 501 | } 502 | 503 | resp, err := c.HTTPClient.Do(req) 504 | if err != nil { 505 | if c.Logger != nil { 506 | c.Logger.LogError(err, options.LogLevel) 507 | } 508 | return nil, err 509 | } 510 | 511 | defer func() { 512 | if cerr := resp.Body.Close(); cerr != nil { 513 | if err != nil { 514 | err = fmt.Errorf("%w; failed to close response body: %v", err, cerr) 515 | } else { 516 | err = fmt.Errorf("failed to close response body: %v", cerr) 517 | } 518 | } 519 | }() 520 | 521 | var responseBody []byte 522 | limitedReader := io.LimitReader(resp.Body, options.MaxContentLength+1) 523 | 524 | if options.OnDownloadProgress != nil { 525 | buf := &bytes.Buffer{} 526 | progressWriter := &ProgressWriter{ 527 | writer: buf, 528 | total: resp.ContentLength, 529 | onProgress: options.OnDownloadProgress, 530 | } 531 | _, err = io.Copy(progressWriter, limitedReader) 532 | if err != nil { 533 | return nil, err 534 | } 535 | responseBody = buf.Bytes() 536 | } else { 537 | responseBody, err = io.ReadAll(limitedReader) 538 | if err != nil { 539 | return nil, err 540 | } 541 | } 542 | 543 | if int64(len(responseBody)) > options.MaxContentLength { 544 | return nil, errors.New("response content length exceeded maxContentLength") 545 | } 546 | 547 | duration := time.Since(startTime) 548 | 549 | if c.Logger != nil { 550 | c.Logger.LogResponse(resp, responseBody, duration, options.LogLevel) 551 | } 552 | 553 | if options.ValidateStatus != nil && !(options.ValidateStatus(resp.StatusCode)) { 554 | return nil, fmt.Errorf("Request failed with status code: %v", resp.StatusCode) 555 | } 556 | 557 | for _, interceptor := range options.InterceptorOptions.ResponseInterceptors { 558 | err = interceptor(resp) 559 | if err != nil { 560 | return nil, fmt.Errorf("response interceptor failed: %w", err) 561 | } 562 | } 563 | 564 | return &Response{ 565 | StatusCode: resp.StatusCode, 566 | Headers: resp.Header, 567 | Body: responseBody, 568 | }, err 569 | } 570 | 571 | func mergeOptions(dst, src *RequestOptions) { 572 | if src.Method != "" { 573 | dst.Method = src.Method 574 | } 575 | if src.URL != "" { 576 | dst.URL = src.URL 577 | } 578 | if src.BaseURL != "" { 579 | dst.BaseURL = src.BaseURL 580 | } 581 | if src.Params != nil { 582 | dst.Params = src.Params 583 | } 584 | if src.Body != nil { 585 | dst.Body = src.Body 586 | } 587 | if src.Headers != nil { 588 | dst.Headers = src.Headers 589 | } 590 | if src.Timeout != 0 { 591 | dst.Timeout = src.Timeout 592 | } 593 | if src.Auth != nil { 594 | dst.Auth = src.Auth 595 | } 596 | if src.ResponseType != "" { 597 | dst.ResponseType = src.ResponseType 598 | } 599 | if src.ResponseEncoding != "" { 600 | dst.ResponseEncoding = src.ResponseEncoding 601 | } 602 | if src.MaxRedirects != 0 { 603 | dst.MaxRedirects = src.MaxRedirects 604 | } 605 | if src.MaxContentLength != 0 { 606 | dst.MaxContentLength = src.MaxContentLength 607 | } 608 | if src.MaxBodyLength != 0 { 609 | dst.MaxBodyLength = src.MaxBodyLength 610 | } 611 | if src.ValidateStatus != nil { 612 | dst.ValidateStatus = src.ValidateStatus 613 | } 614 | if src.InterceptorOptions.RequestInterceptors != nil { 615 | dst.InterceptorOptions.RequestInterceptors = src.InterceptorOptions.RequestInterceptors 616 | } 617 | if src.InterceptorOptions.ResponseInterceptors != nil { 618 | dst.InterceptorOptions.ResponseInterceptors = src.InterceptorOptions.ResponseInterceptors 619 | } 620 | if src.OnUploadProgress != nil { 621 | dst.OnUploadProgress = src.OnUploadProgress 622 | } 623 | if src.OnDownloadProgress != nil { 624 | dst.OnDownloadProgress = src.OnDownloadProgress 625 | } 626 | if src.Proxy != nil { 627 | dst.Proxy = src.Proxy 628 | } 629 | dst.Decompress = src.Decompress 630 | } 631 | 632 | func SetBaseURL(baseURL string) { 633 | defaultClient.BaseURL = baseURL 634 | } 635 | 636 | func NewClient(baseURL string) *Client { 637 | return &Client{ 638 | BaseURL: baseURL, 639 | HTTPClient: &http.Client{}, 640 | Logger: NewLogger(LevelNone), 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /axios4go_test.go: -------------------------------------------------------------------------------- 1 | package axios4go 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func setupTestServer() *httptest.Server { 17 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | switch r.URL.Path { 19 | case "/get": 20 | json.NewEncoder(w).Encode(map[string]string{"message": "get success"}) 21 | case "/getByProxy": 22 | json.NewEncoder(w).Encode(map[string]string{"message": "get success by proxy"}) 23 | case "/post": 24 | json.NewEncoder(w).Encode(map[string]string{"message": "post success"}) 25 | case "/put": 26 | json.NewEncoder(w).Encode(map[string]string{"message": "put success"}) 27 | case "/delete": 28 | json.NewEncoder(w).Encode(map[string]string{"message": "delete success"}) 29 | case "/head": 30 | w.Header().Set("X-Test-Header", "test-value") 31 | case "/options": 32 | w.Header().Set("Allow", "GET, POST, PUT, DELETE, HEAD, OPTIONS") 33 | case "/patch": 34 | json.NewEncoder(w).Encode(map[string]string{"message": "patch success"}) 35 | default: 36 | http.NotFound(w, r) 37 | } 38 | })) 39 | } 40 | 41 | func TestGet(t *testing.T) { 42 | server := setupTestServer() 43 | defer server.Close() 44 | 45 | t.Run("Simple Style", func(t *testing.T) { 46 | response, err := Get(server.URL + "/get") 47 | if err != nil { 48 | t.Fatalf("Expected no error, got %v", err) 49 | } 50 | 51 | if response.StatusCode != http.StatusOK { 52 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 53 | } 54 | 55 | var result map[string]string 56 | err = json.Unmarshal(response.Body, &result) 57 | if err != nil { 58 | t.Fatalf("Error unmarshaling response body: %v", err) 59 | } 60 | 61 | if result["message"] != "get success" { 62 | t.Errorf("Expected message 'get success', got '%s'", result["message"]) 63 | } 64 | }) 65 | 66 | t.Run("Promise Style", func(t *testing.T) { 67 | promise := GetAsync(server.URL + "/get") 68 | var thenExecuted, finallyExecuted bool 69 | 70 | promise. 71 | Then(func(response *Response) { 72 | thenExecuted = true 73 | }). 74 | Catch(func(err error) { 75 | t.Errorf("Expected no error, got %v", err) 76 | }). 77 | Finally(func() { 78 | finallyExecuted = true 79 | }) 80 | 81 | <-promise.done 82 | 83 | if !thenExecuted { 84 | t.Error("Then was not executed") 85 | } 86 | if !finallyExecuted { 87 | t.Error("Finally was not executed") 88 | } 89 | }) 90 | 91 | t.Run("Request Style", func(t *testing.T) { 92 | response, err := Request("GET", server.URL+"/get") 93 | if err != nil { 94 | t.Fatalf("Expected no error, got %v", err) 95 | } 96 | 97 | if response.StatusCode != http.StatusOK { 98 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 99 | } 100 | 101 | var result map[string]string 102 | err = json.Unmarshal(response.Body, &result) 103 | if err != nil { 104 | t.Fatalf("Error unmarshaling response body: %v", err) 105 | } 106 | 107 | if result["message"] != "get success" { 108 | t.Errorf("Expected message 'get success', got '%s'", result["message"]) 109 | } 110 | }) 111 | } 112 | 113 | func TestPost(t *testing.T) { 114 | server := setupTestServer() 115 | defer server.Close() 116 | 117 | t.Run("Simple Style", func(t *testing.T) { 118 | body := map[string]string{"key": "value"} 119 | response, err := Post(server.URL+"/post", body) 120 | if err != nil { 121 | t.Fatalf("Expected no error, got %v", err) 122 | } 123 | 124 | if response.StatusCode != http.StatusOK { 125 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 126 | } 127 | 128 | var result map[string]string 129 | err = json.Unmarshal(response.Body, &result) 130 | if err != nil { 131 | t.Fatalf("Error unmarshaling response body: %v", err) 132 | } 133 | 134 | if result["message"] != "post success" { 135 | t.Errorf("Expected message 'post success', got '%s'", result["message"]) 136 | } 137 | }) 138 | 139 | t.Run("Promise Style", func(t *testing.T) { 140 | body := map[string]string{"key": "value"} 141 | 142 | promise := PostAsync(server.URL+"/post", body) 143 | 144 | var thenExecuted, finallyExecuted bool 145 | 146 | promise. 147 | Then(func(response *Response) { 148 | if response.StatusCode != http.StatusOK { 149 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 150 | } 151 | 152 | var result map[string]string 153 | err := json.Unmarshal(response.Body, &result) 154 | if err != nil { 155 | t.Errorf("Error unmarshaling response body: %v", err) 156 | } 157 | 158 | if result["message"] != "post success" { 159 | t.Errorf("Expected message 'post success', got '%s'", result["message"]) 160 | } 161 | thenExecuted = true 162 | }). 163 | Catch(func(err error) { 164 | t.Errorf("Expected no error, got %v", err) 165 | }). 166 | Finally(func() { 167 | finallyExecuted = true 168 | }) 169 | 170 | <-promise.done 171 | 172 | if !thenExecuted { 173 | t.Error("Then was not executed") 174 | } 175 | if !finallyExecuted { 176 | t.Error("Finally was not executed") 177 | } 178 | }) 179 | 180 | t.Run("Request Style", func(t *testing.T) { 181 | body := map[string]string{"key": "value"} 182 | response, err := Request("POST", server.URL+"/post", &RequestOptions{Body: body}) 183 | if err != nil { 184 | t.Fatalf("Expected no error, got %v", err) 185 | } 186 | 187 | if response.StatusCode != http.StatusOK { 188 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 189 | } 190 | 191 | var result map[string]string 192 | err = json.Unmarshal(response.Body, &result) 193 | if err != nil { 194 | t.Fatalf("Error unmarshaling response body: %v", err) 195 | } 196 | 197 | if result["message"] != "post success" { 198 | t.Errorf("Expected message 'post success', got '%s'", result["message"]) 199 | } 200 | }) 201 | } 202 | 203 | func TestPut(t *testing.T) { 204 | server := setupTestServer() 205 | defer server.Close() 206 | 207 | t.Run("Simple Style", func(t *testing.T) { 208 | body := map[string]string{"key": "updated_value"} 209 | response, err := Put(server.URL+"/put", body) 210 | if err != nil { 211 | t.Fatalf("Expected no error, got %v", err) 212 | } 213 | 214 | if response.StatusCode != http.StatusOK { 215 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 216 | } 217 | 218 | var result map[string]string 219 | err = json.Unmarshal(response.Body, &result) 220 | if err != nil { 221 | t.Fatalf("Error unmarshaling response body: %v", err) 222 | } 223 | 224 | if result["message"] != "put success" { 225 | t.Errorf("Expected message 'put success', got '%s'", result["message"]) 226 | } 227 | }) 228 | 229 | t.Run("Promise Style", func(t *testing.T) { 230 | body := map[string]string{"key": "updated_value"} 231 | 232 | promise := PutAsync(server.URL+"/put", body) 233 | 234 | var thenExecuted, finallyExecuted bool 235 | 236 | promise. 237 | Then(func(response *Response) { 238 | if response.StatusCode != http.StatusOK { 239 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 240 | } 241 | 242 | var result map[string]string 243 | err := json.Unmarshal(response.Body, &result) 244 | if err != nil { 245 | t.Errorf("Error unmarshaling response body: %v", err) 246 | } 247 | 248 | if result["message"] != "put success" { 249 | t.Errorf("Expected message 'put success', got '%s'", result["message"]) 250 | } 251 | thenExecuted = true 252 | }). 253 | Catch(func(err error) { 254 | t.Errorf("Expected no error, got %v", err) 255 | }). 256 | Finally(func() { 257 | finallyExecuted = true 258 | }) 259 | 260 | <-promise.done 261 | 262 | if !thenExecuted { 263 | t.Error("Then was not executed") 264 | } 265 | if !finallyExecuted { 266 | t.Error("Finally was not executed") 267 | } 268 | }) 269 | 270 | t.Run("Request Style", func(t *testing.T) { 271 | body := map[string]string{"key": "updated_value"} 272 | response, err := Request("PUT", server.URL+"/put", &RequestOptions{Body: body}) 273 | if err != nil { 274 | t.Fatalf("Expected no error, got %v", err) 275 | } 276 | 277 | if response.StatusCode != http.StatusOK { 278 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 279 | } 280 | 281 | var result map[string]string 282 | err = json.Unmarshal(response.Body, &result) 283 | if err != nil { 284 | t.Fatalf("Error unmarshaling response body: %v", err) 285 | } 286 | 287 | if result["message"] != "put success" { 288 | t.Errorf("Expected message 'put success', got '%s'", result["message"]) 289 | } 290 | }) 291 | } 292 | 293 | func TestDelete(t *testing.T) { 294 | server := setupTestServer() 295 | defer server.Close() 296 | 297 | t.Run("Simple Style", func(t *testing.T) { 298 | response, err := Delete(server.URL + "/delete") 299 | if err != nil { 300 | t.Fatalf("Expected no error, got %v", err) 301 | } 302 | 303 | if response.StatusCode != http.StatusOK { 304 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 305 | } 306 | 307 | var result map[string]string 308 | err = json.Unmarshal(response.Body, &result) 309 | if err != nil { 310 | t.Fatalf("Error unmarshaling response body: %v", err) 311 | } 312 | 313 | if result["message"] != "delete success" { 314 | t.Errorf("Expected message 'delete success', got '%s'", result["message"]) 315 | } 316 | }) 317 | 318 | t.Run("Promise Style", func(t *testing.T) { 319 | promise := DeleteAsync(server.URL + "/delete") 320 | 321 | var thenExecuted, finallyExecuted bool 322 | 323 | promise. 324 | Then(func(response *Response) { 325 | if response.StatusCode != http.StatusOK { 326 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 327 | } 328 | 329 | var result map[string]string 330 | err := json.Unmarshal(response.Body, &result) 331 | if err != nil { 332 | t.Errorf("Error unmarshaling response body: %v", err) 333 | } 334 | 335 | if result["message"] != "delete success" { 336 | t.Errorf("Expected message 'delete success', got '%s'", result["message"]) 337 | } 338 | thenExecuted = true 339 | }). 340 | Catch(func(err error) { 341 | t.Errorf("Expected no error, got %v", err) 342 | }). 343 | Finally(func() { 344 | finallyExecuted = true 345 | }) 346 | 347 | <-promise.done 348 | 349 | if !thenExecuted { 350 | t.Error("Then was not executed") 351 | } 352 | if !finallyExecuted { 353 | t.Error("Finally was not executed") 354 | } 355 | }) 356 | 357 | t.Run("Request Style", func(t *testing.T) { 358 | response, err := Request("DELETE", server.URL+"/delete") 359 | if err != nil { 360 | t.Fatalf("Expected no error, got %v", err) 361 | } 362 | 363 | if response.StatusCode != http.StatusOK { 364 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 365 | } 366 | 367 | var result map[string]string 368 | err = json.Unmarshal(response.Body, &result) 369 | if err != nil { 370 | t.Fatalf("Error unmarshaling response body: %v", err) 371 | } 372 | 373 | if result["message"] != "delete success" { 374 | t.Errorf("Expected message 'delete success', got '%s'", result["message"]) 375 | } 376 | }) 377 | } 378 | 379 | func TestHead(t *testing.T) { 380 | server := setupTestServer() 381 | defer server.Close() 382 | 383 | t.Run("Simple Style", func(t *testing.T) { 384 | response, err := Head(server.URL + "/head") 385 | if err != nil { 386 | t.Fatalf("Expected no error, got %v", err) 387 | } 388 | 389 | if response.StatusCode != http.StatusOK { 390 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 391 | } 392 | 393 | if response.Headers.Get("X-Test-Header") != "test-value" { 394 | t.Errorf("Expected X-Test-Header to be 'test-value', got '%s'", response.Headers.Get("X-Test-Header")) 395 | } 396 | 397 | if len(response.Body) != 0 { 398 | t.Errorf("Expected empty body, got %d bytes", len(response.Body)) 399 | } 400 | }) 401 | 402 | t.Run("Promise Style", func(t *testing.T) { 403 | promise := HeadAsync(server.URL + "/head") 404 | 405 | var thenExecuted, finallyExecuted bool 406 | 407 | promise. 408 | Then(func(response *Response) { 409 | if response.StatusCode != http.StatusOK { 410 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 411 | } 412 | 413 | if response.Headers.Get("X-Test-Header") != "test-value" { 414 | t.Errorf("Expected X-Test-Header to be 'test-value', got '%s'", response.Headers.Get("X-Test-Header")) 415 | } 416 | 417 | if len(response.Body) != 0 { 418 | t.Errorf("Expected empty body, got %d bytes", len(response.Body)) 419 | } 420 | thenExecuted = true 421 | }). 422 | Catch(func(err error) { 423 | t.Errorf("Expected no error, got %v", err) 424 | }). 425 | Finally(func() { 426 | finallyExecuted = true 427 | }) 428 | 429 | <-promise.done 430 | 431 | if !thenExecuted { 432 | t.Error("Then was not executed") 433 | } 434 | if !finallyExecuted { 435 | t.Error("Finally was not executed") 436 | } 437 | }) 438 | 439 | t.Run("Request Style", func(t *testing.T) { 440 | response, err := Request("HEAD", server.URL+"/head") 441 | if err != nil { 442 | t.Fatalf("Expected no error, got %v", err) 443 | } 444 | 445 | if response.StatusCode != http.StatusOK { 446 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 447 | } 448 | 449 | if response.Headers.Get("X-Test-Header") != "test-value" { 450 | t.Errorf("Expected X-Test-Header to be 'test-value', got '%s'", response.Headers.Get("X-Test-Header")) 451 | } 452 | 453 | if len(response.Body) != 0 { 454 | t.Errorf("Expected empty body, got %d bytes", len(response.Body)) 455 | } 456 | }) 457 | } 458 | 459 | func TestOptions(t *testing.T) { 460 | server := setupTestServer() 461 | defer server.Close() 462 | 463 | expectedAllowHeader := "GET, POST, PUT, DELETE, HEAD, OPTIONS" 464 | 465 | t.Run("Simple Style", func(t *testing.T) { 466 | response, err := Options(server.URL + "/options") 467 | if err != nil { 468 | t.Fatalf("Expected no error, got %v", err) 469 | } 470 | 471 | if response.StatusCode != http.StatusOK { 472 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 473 | } 474 | 475 | allowHeader := response.Headers.Get("Allow") 476 | if allowHeader != expectedAllowHeader { 477 | t.Errorf("Expected Allow header to be '%s', got '%s'", expectedAllowHeader, allowHeader) 478 | } 479 | 480 | if len(response.Body) != 0 { 481 | t.Errorf("Expected empty body, got %d bytes", len(response.Body)) 482 | } 483 | }) 484 | 485 | t.Run("Promise Style", func(t *testing.T) { 486 | promise := OptionsAsync(server.URL + "/options") 487 | 488 | var thenExecuted, finallyExecuted bool 489 | 490 | promise. 491 | Then(func(response *Response) { 492 | if response.StatusCode != http.StatusOK { 493 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 494 | } 495 | 496 | allowHeader := response.Headers.Get("Allow") 497 | if allowHeader != expectedAllowHeader { 498 | t.Errorf("Expected Allow header to be '%s', got '%s'", expectedAllowHeader, allowHeader) 499 | } 500 | 501 | if len(response.Body) != 0 { 502 | t.Errorf("Expected empty body, got %d bytes", len(response.Body)) 503 | } 504 | thenExecuted = true 505 | }). 506 | Catch(func(err error) { 507 | t.Errorf("Expected no error, got %v", err) 508 | }). 509 | Finally(func() { 510 | finallyExecuted = true 511 | }) 512 | 513 | <-promise.done 514 | 515 | if !thenExecuted { 516 | t.Error("Then was not executed") 517 | } 518 | if !finallyExecuted { 519 | t.Error("Finally was not executed") 520 | } 521 | }) 522 | 523 | t.Run("Request Style", func(t *testing.T) { 524 | response, err := Request("OPTIONS", server.URL+"/options") 525 | if err != nil { 526 | t.Fatalf("Expected no error, got %v", err) 527 | } 528 | 529 | if response.StatusCode != http.StatusOK { 530 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 531 | } 532 | 533 | allowHeader := response.Headers.Get("Allow") 534 | if allowHeader != expectedAllowHeader { 535 | t.Errorf("Expected Allow header to be '%s', got '%s'", expectedAllowHeader, allowHeader) 536 | } 537 | 538 | if len(response.Body) != 0 { 539 | t.Errorf("Expected empty body, got %d bytes", len(response.Body)) 540 | } 541 | }) 542 | } 543 | 544 | func TestPatch(t *testing.T) { 545 | server := setupTestServer() 546 | defer server.Close() 547 | 548 | t.Run("Simple Style", func(t *testing.T) { 549 | body := map[string]string{"key": "patched_value"} 550 | response, err := Patch(server.URL+"/patch", body) 551 | if err != nil { 552 | t.Fatalf("Expected no error, got %v", err) 553 | } 554 | 555 | if response.StatusCode != http.StatusOK { 556 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 557 | } 558 | 559 | var result map[string]string 560 | err = json.Unmarshal(response.Body, &result) 561 | if err != nil { 562 | t.Fatalf("Error unmarshaling response body: %v", err) 563 | } 564 | 565 | if result["message"] != "patch success" { 566 | t.Errorf("Expected message 'patch success', got '%s'", result["message"]) 567 | } 568 | }) 569 | 570 | t.Run("Promise Style", func(t *testing.T) { 571 | body := map[string]string{"key": "patched_value"} 572 | 573 | promise := PatchAsync(server.URL+"/patch", body) 574 | 575 | var thenExecuted, finallyExecuted bool 576 | 577 | promise. 578 | Then(func(response *Response) { 579 | if response.StatusCode != http.StatusOK { 580 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 581 | } 582 | 583 | var result map[string]string 584 | err := json.Unmarshal(response.Body, &result) 585 | if err != nil { 586 | t.Errorf("Error unmarshaling response body: %v", err) 587 | } 588 | 589 | if result["message"] != "patch success" { 590 | t.Errorf("Expected message 'patch success', got '%s'", result["message"]) 591 | } 592 | thenExecuted = true 593 | }). 594 | Catch(func(err error) { 595 | t.Errorf("Expected no error, got %v", err) 596 | }). 597 | Finally(func() { 598 | finallyExecuted = true 599 | }) 600 | 601 | <-promise.done 602 | 603 | if !thenExecuted { 604 | t.Error("Then was not executed") 605 | } 606 | if !finallyExecuted { 607 | t.Error("Finally was not executed") 608 | } 609 | }) 610 | 611 | t.Run("Request Style", func(t *testing.T) { 612 | body := map[string]string{"key": "patched_value"} 613 | response, err := Request("PATCH", server.URL+"/patch", &RequestOptions{Body: body}) 614 | if err != nil { 615 | t.Fatalf("Expected no error, got %v", err) 616 | } 617 | 618 | if response.StatusCode != http.StatusOK { 619 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 620 | } 621 | 622 | var result map[string]string 623 | err = json.Unmarshal(response.Body, &result) 624 | if err != nil { 625 | t.Fatalf("Error unmarshaling response body: %v", err) 626 | } 627 | 628 | if result["message"] != "patch success" { 629 | t.Errorf("Expected message 'patch success', got '%s'", result["message"]) 630 | } 631 | }) 632 | } 633 | 634 | func TestValidateStatus(t *testing.T) { 635 | server := setupTestServer() 636 | defer server.Close() 637 | reqOptions := &RequestOptions{ 638 | ValidateStatus: func(StatusCode int) bool { 639 | if StatusCode == 200 { 640 | return false 641 | } 642 | return true 643 | }, 644 | } 645 | 646 | t.Run("Simple Style", func(t *testing.T) { 647 | response, err := Get(server.URL+"/get", reqOptions) 648 | if err == nil || response != nil { 649 | t.Fatalf("Expected error, got %v", err) 650 | } 651 | if err.Error() != "Request failed with status code: 200" { 652 | t.Errorf("Expected error Request failed with status code: 200, got %v", err.Error()) 653 | } 654 | }) 655 | 656 | t.Run("Promise Style", func(t *testing.T) { 657 | promise := GetAsync(server.URL+"/get", reqOptions) 658 | 659 | var catchExecuted, finallyExecuted bool 660 | 661 | promise. 662 | Then(func(response *Response) { 663 | t.Error("Then should not be executed when validateStatus returns false") 664 | }). 665 | Catch(func(err error) { 666 | if err == nil { 667 | t.Fatal("Expected an error, got nil") 668 | } 669 | expectedError := "Request failed with status code: 200" 670 | if err.Error() != expectedError { 671 | t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) 672 | } 673 | catchExecuted = true 674 | }). 675 | Finally(func() { 676 | finallyExecuted = true 677 | }) 678 | 679 | <-promise.done 680 | 681 | if !catchExecuted { 682 | t.Error("Catch was not executed") 683 | } 684 | if !finallyExecuted { 685 | t.Error("Finally was not executed") 686 | } 687 | }) 688 | 689 | t.Run("Request Style", func(t *testing.T) { 690 | response, err := Request("GET", server.URL+"/get", reqOptions) 691 | if err == nil || response != nil { 692 | t.Fatalf("Expected error, got %v", err) 693 | } 694 | if err.Error() != "Request failed with status code: 200" { 695 | t.Errorf("Expected error Request failed with status code: 200, got %v", err.Error()) 696 | } 697 | }) 698 | } 699 | 700 | func TestInterceptors(t *testing.T) { 701 | server := setupTestServer() 702 | defer server.Close() 703 | 704 | var interceptedRequest *http.Request 705 | requestInterceptorCalled := false 706 | requestInterceptor := func(req *http.Request) error { 707 | req.Header.Set("X-Intercepted", "true") 708 | interceptedRequest = req 709 | requestInterceptorCalled = true 710 | return nil 711 | } 712 | 713 | responseInterceptor := func(resp *http.Response) error { 714 | resp.Header.Set("X-Intercepted-Response", "true") 715 | return nil 716 | } 717 | 718 | opts := &RequestOptions{ 719 | Headers: map[string]string{ 720 | "Content-Type": "application/json", 721 | }, 722 | Params: map[string]string{ 723 | "query": "myQuery", 724 | }, 725 | } 726 | 727 | opts.InterceptorOptions = InterceptorOptions{ 728 | RequestInterceptors: []func(*http.Request) error{requestInterceptor}, 729 | ResponseInterceptors: []func(*http.Response) error{responseInterceptor}, 730 | } 731 | 732 | t.Run("Interceptors Test", func(t *testing.T) { 733 | response, err := Get(server.URL+"/get", opts) 734 | if err != nil { 735 | t.Fatalf("Expected no error, got %v", err) 736 | } 737 | 738 | if !requestInterceptorCalled { 739 | t.Error("Request interceptor was not called") 740 | } 741 | 742 | if interceptedRequest != nil { 743 | if interceptedRequest.Header.Get("X-Intercepted") != "true" { 744 | t.Errorf("Expected request header 'X-Intercepted' to be 'true', got '%s'", interceptedRequest.Header.Get("X-Intercepted")) 745 | } 746 | } else { 747 | t.Error("Intercepted request is nil") 748 | } 749 | 750 | if response.Headers.Get("X-Intercepted-Response") != "true" { 751 | t.Errorf("Expected response header 'X-Intercepted-Response' to be 'true', got '%s'", response.Headers.Get("X-Intercepted-Response")) 752 | } 753 | }) 754 | } 755 | 756 | func handler(w http.ResponseWriter, r *http.Request) { 757 | request, err := http.NewRequest(r.Method, r.RequestURI, nil) 758 | client := http.Client{} 759 | response, err := client.Do(request) 760 | if err != nil { 761 | return 762 | } 763 | bytes := make([]byte, response.ContentLength) 764 | response.Body.Read(bytes) 765 | w.Write(bytes) 766 | } 767 | 768 | func TestGetByProxy(t *testing.T) { 769 | server := setupTestServer() 770 | defer server.Close() 771 | path := "/getByProxy" 772 | http.HandleFunc("/", handler) 773 | go func() { 774 | err := http.ListenAndServe(":8080", nil) 775 | if err != nil { 776 | return 777 | } 778 | }() 779 | t.Run("Simple Style", func(t *testing.T) { 780 | response, err := Get(server.URL+path, 781 | &RequestOptions{ 782 | Proxy: &Proxy{ 783 | Protocol: "http", 784 | Host: "localhost", 785 | Port: 8080, 786 | Auth: &Auth{ 787 | Username: "username", 788 | Password: "password", 789 | }, 790 | }, 791 | }, 792 | ) 793 | if err != nil { 794 | t.Fatalf("Expected no error, got %v", err) 795 | } 796 | 797 | if response.StatusCode != http.StatusOK { 798 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 799 | } 800 | 801 | var result map[string]string 802 | err = json.Unmarshal(response.Body, &result) 803 | if err != nil { 804 | t.Fatalf("Error unmarshaling response Body: %v", err) 805 | } 806 | 807 | if result["message"] != "get success by proxy" { 808 | t.Errorf("Expected message 'get success by proxy', got '%s'", result["message"]) 809 | } 810 | }) 811 | 812 | t.Run("Promise Style", func(t *testing.T) { 813 | promise := GetAsync(server.URL + path) 814 | var thenExecuted, finallyExecuted bool 815 | 816 | promise. 817 | Then(func(response *Response) { 818 | thenExecuted = true 819 | }). 820 | Catch(func(err error) { 821 | t.Errorf("Expected no error, got %v", err) 822 | }). 823 | Finally(func() { 824 | finallyExecuted = true 825 | }) 826 | 827 | <-promise.done 828 | 829 | if !thenExecuted { 830 | t.Error("Then was not executed") 831 | } 832 | if !finallyExecuted { 833 | t.Error("Finally was not executed") 834 | } 835 | }) 836 | 837 | t.Run("Request Style", func(t *testing.T) { 838 | response, err := Request("GET", server.URL+path) 839 | if err != nil { 840 | t.Fatalf("Expected no error, got %v", err) 841 | } 842 | 843 | if response.StatusCode != http.StatusOK { 844 | t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) 845 | } 846 | 847 | var result map[string]string 848 | err = json.Unmarshal(response.Body, &result) 849 | if err != nil { 850 | t.Fatalf("Error unmarshaling response Body: %v", err) 851 | } 852 | 853 | if result["message"] != "get success by proxy" { 854 | t.Errorf("Expected message 'get success by proxy', got '%s'", result["message"]) 855 | } 856 | }) 857 | } 858 | 859 | func TestProgressCallbacks(t *testing.T) { 860 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 861 | _, err := io.Copy(io.Discard, r.Body) 862 | if err != nil { 863 | t.Fatalf("Failed to read request body: %v", err) 864 | } 865 | 866 | w.Header().Set("Content-Length", "1000000") 867 | for i := 0; i < 1000000; i++ { 868 | _, err := w.Write([]byte("a")) 869 | if err != nil { 870 | t.Fatalf("Failed to write response: %v", err) 871 | } 872 | } 873 | })) 874 | defer server.Close() 875 | 876 | uploadCalled := false 877 | downloadCalled := false 878 | 879 | body := bytes.NewReader([]byte(strings.Repeat("b", 500000))) // 500KB upload 880 | 881 | _, err := Post(server.URL, body, &RequestOptions{ 882 | OnUploadProgress: func(bytesRead, totalBytes int64) { 883 | uploadCalled = true 884 | if bytesRead > totalBytes { 885 | t.Errorf("Upload progress: bytesRead (%d) > totalBytes (%d)", bytesRead, totalBytes) 886 | } 887 | }, 888 | OnDownloadProgress: func(bytesRead, totalBytes int64) { 889 | downloadCalled = true 890 | if bytesRead > totalBytes { 891 | t.Errorf("Download progress: bytesRead (%d) > totalBytes (%d)", bytesRead, totalBytes) 892 | } 893 | }, 894 | MaxContentLength: 2000000, // Set this to allow our 1MB response 895 | }) 896 | if err != nil { 897 | t.Fatalf("Expected no error, got %v", err) 898 | } 899 | 900 | if !uploadCalled { 901 | t.Error("Upload progress callback was not called") 902 | } 903 | if !downloadCalled { 904 | t.Error("Download progress callback was not called") 905 | } 906 | } 907 | 908 | func TestLogging(t *testing.T) { 909 | server := setupTestServer() 910 | defer server.Close() 911 | 912 | t.Run("Test Logger Integration", func(t *testing.T) { 913 | var buf bytes.Buffer 914 | logger := NewDefaultLogger(LogOptions{ 915 | Level: LevelDebug, 916 | Output: &buf, 917 | IncludeBody: true, 918 | IncludeHeaders: true, 919 | MaskHeaders: []string{"Authorization"}, 920 | }) 921 | 922 | client := &Client{ 923 | HTTPClient: &http.Client{}, 924 | Logger: logger, 925 | } 926 | 927 | // Test with sensitive headers 928 | reqOptions := &RequestOptions{ 929 | Method: "POST", 930 | URL: server.URL + "/post", 931 | LogLevel: LevelDebug, 932 | Headers: map[string]string{ 933 | "Authorization": "Bearer secret-token", 934 | "X-Test": "test-value", 935 | }, 936 | Body: map[string]string{"test": "data"}, 937 | MaxContentLength: 2000, 938 | } 939 | 940 | _, err := client.Request(reqOptions) 941 | if err != nil { 942 | t.Fatalf("Request failed: %v", err) 943 | } 944 | 945 | logOutput := buf.String() 946 | 947 | if !strings.Contains(logOutput, "REQUEST: POST") { 948 | t.Error("Log should contain request method") 949 | } 950 | if !strings.Contains(logOutput, "[MASKED]") { 951 | t.Error("Authorization header should be masked") 952 | } 953 | if !strings.Contains(logOutput, "test-value") { 954 | t.Error("Non-sensitive header should be visible") 955 | } 956 | if !strings.Contains(logOutput, "test") { 957 | t.Error("Request body should be logged") 958 | } 959 | 960 | if !strings.Contains(logOutput, "RESPONSE: 200") { 961 | t.Error("Log should contain response status") 962 | } 963 | if !strings.Contains(logOutput, "post success") { 964 | t.Error("Response body should be logged") 965 | } 966 | }) 967 | 968 | t.Run("Test Log Levels", func(t *testing.T) { 969 | var buf bytes.Buffer 970 | logger := NewDefaultLogger(LogOptions{ 971 | Level: LevelError, 972 | Output: &buf, 973 | }) 974 | 975 | client := &Client{ 976 | HTTPClient: &http.Client{}, 977 | Logger: logger, 978 | } 979 | 980 | _, err := client.Request(&RequestOptions{ 981 | Method: "GET", 982 | URL: server.URL + "/get", 983 | LogLevel: LevelDebug, 984 | MaxContentLength: 2000, 985 | }) 986 | if err != nil { 987 | t.Fatalf("Request failed: %v", err) 988 | } 989 | 990 | if buf.Len() > 0 { 991 | t.Error("Debug level request should not be logged when logger is at Error level") 992 | } 993 | }) 994 | } 995 | 996 | func TestTimeoutHandling(t *testing.T) { 997 | slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 998 | time.Sleep(2 * time.Second) 999 | w.WriteHeader(http.StatusOK) 1000 | w.Write([]byte(`{"message": "slow response"}`)) 1001 | })) 1002 | defer slowServer.Close() 1003 | 1004 | start := time.Now() 1005 | response, err := Get(slowServer.URL, &RequestOptions{ 1006 | Timeout: 1000, 1007 | }) 1008 | elapsed := time.Since(start) 1009 | 1010 | if err == nil { 1011 | t.Fatalf("Expected a timeout error, but got no error and response: %v", response) 1012 | } 1013 | 1014 | if elapsed >= 2*time.Second { 1015 | t.Errorf("Expected to fail before 2 seconds, but took %v", elapsed) 1016 | } 1017 | 1018 | t.Logf("Timeout test passed with error: %v", err) 1019 | } 1020 | 1021 | func TestMaxRedirects(t *testing.T) { 1022 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1023 | switch r.URL.Path { 1024 | case "/redirect1": 1025 | // Return a 301 or 302 redirect to /redirect2 1026 | http.Redirect(w, r, "/redirect2", http.StatusFound) 1027 | case "/redirect2": 1028 | // Return a 301 or 302 redirect to /final 1029 | http.Redirect(w, r, "/final", http.StatusFound) 1030 | case "/final": 1031 | // Final destination 1032 | w.WriteHeader(http.StatusOK) 1033 | w.Write([]byte(`{"message":"final destination"}`)) 1034 | default: 1035 | // Return 404 if the path is not one of the above 1036 | http.NotFound(w, r) 1037 | } 1038 | })) 1039 | defer server.Close() 1040 | 1041 | t.Run("FollowRedirects", func(t *testing.T) { 1042 | resp, err := Get(server.URL+"/redirect1", &RequestOptions{ 1043 | MaxRedirects: 5, 1044 | }) 1045 | if err != nil { 1046 | t.Fatalf("Expected no error, got: %v", err) 1047 | } 1048 | if resp.StatusCode != http.StatusOK { 1049 | t.Errorf("Expected status code 200, got %d", resp.StatusCode) 1050 | } 1051 | var result map[string]string 1052 | if err := json.Unmarshal(resp.Body, &result); err != nil { 1053 | t.Fatalf("Error parsing final JSON: %v", err) 1054 | } 1055 | if result["message"] != "final destination" { 1056 | t.Errorf(`Expected "final destination", got %q`, result["message"]) 1057 | } 1058 | }) 1059 | 1060 | t.Run("TooManyRedirects", func(t *testing.T) { 1061 | resp, err := Get(server.URL+"/redirect1", &RequestOptions{ 1062 | MaxRedirects: 1, 1063 | }) 1064 | if err == nil { 1065 | t.Fatalf("Expected error due to too many redirects, but got response: %v", resp) 1066 | } 1067 | t.Logf("Redirect test got error as expected: %v", err) 1068 | }) 1069 | } 1070 | 1071 | func TestBaseURL(t *testing.T) { 1072 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1073 | w.WriteHeader(http.StatusOK) 1074 | w.Write([]byte(r.URL.Path)) 1075 | })) 1076 | defer server.Close() 1077 | 1078 | t.Run("Using DefaultClient with SetBaseURL", func(t *testing.T) { 1079 | SetBaseURL(server.URL + "/api") 1080 | defer SetBaseURL("") 1081 | 1082 | resp, err := Get("/testBaseUrl") 1083 | if err != nil { 1084 | t.Fatalf("Expected no error, got %v", err) 1085 | } 1086 | if resp.StatusCode != http.StatusOK { 1087 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1088 | } 1089 | 1090 | if string(resp.Body) != "/api/testBaseUrl" { 1091 | t.Errorf("Expected path '/api/testBaseUrl', got %q", string(resp.Body)) 1092 | } 1093 | }) 1094 | 1095 | t.Run("Using NewClient with BaseURL", func(t *testing.T) { 1096 | client := NewClient(server.URL + "/prefix") 1097 | 1098 | resp, err := client.Request(&RequestOptions{ 1099 | Method: "GET", 1100 | URL: "hello", 1101 | }) 1102 | if err != nil { 1103 | t.Fatalf("Expected no error, got %v", err) 1104 | } 1105 | if resp.StatusCode != http.StatusOK { 1106 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1107 | } 1108 | 1109 | if string(resp.Body) != "/prefix/hello" { 1110 | t.Errorf("Expected path '/prefix/hello', got %q", string(resp.Body)) 1111 | } 1112 | }) 1113 | 1114 | t.Run("TrailingSlashInBaseURL", func(t *testing.T) { 1115 | client := NewClient(server.URL + "/api/") 1116 | 1117 | resp, err := client.Request(&RequestOptions{ 1118 | Method: "GET", 1119 | URL: "/user/123", 1120 | }) 1121 | if err != nil { 1122 | t.Fatalf("Expected no error, got %v", err) 1123 | } 1124 | if resp.StatusCode != http.StatusOK { 1125 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1126 | } 1127 | 1128 | if string(resp.Body) != "/api/user/123" { 1129 | t.Errorf("Expected path '/api/user/123', got %q", string(resp.Body)) 1130 | } 1131 | }) 1132 | } 1133 | 1134 | func TestBasicAuth(t *testing.T) { 1135 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1136 | auth := r.Header.Get("Authorization") 1137 | if auth == "" { 1138 | http.Error(w, "Missing Authorization header", http.StatusUnauthorized) 1139 | return 1140 | } 1141 | 1142 | // "user:pass" => "Basic dXNlcjpwYXNz" 1143 | expected := "Basic dXNlcjpwYXNz" 1144 | if auth == expected { 1145 | w.WriteHeader(http.StatusOK) 1146 | w.Write([]byte(`{"status":"authorized"}`)) 1147 | } else { 1148 | http.Error(w, "Invalid credentials", http.StatusUnauthorized) 1149 | } 1150 | })) 1151 | defer server.Close() 1152 | 1153 | t.Run("Correct Credentials", func(t *testing.T) { 1154 | opts := &RequestOptions{ 1155 | Auth: &Auth{ 1156 | Username: "user", 1157 | Password: "pass", 1158 | }, 1159 | } 1160 | resp, err := Get(server.URL, opts) 1161 | if err != nil { 1162 | t.Fatalf("Unexpected error: %v", err) 1163 | } 1164 | if resp.StatusCode != http.StatusOK { 1165 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1166 | } 1167 | if string(resp.Body) != `{"status":"authorized"}` { 1168 | t.Errorf("Expected body to be {\"status\":\"authorized\"}, got %s", resp.Body) 1169 | } 1170 | }) 1171 | 1172 | t.Run("Incorrect Credentials", func(t *testing.T) { 1173 | opts := &RequestOptions{ 1174 | Auth: &Auth{ 1175 | Username: "baduser", 1176 | Password: "wrongpass", 1177 | }, 1178 | } 1179 | resp, err := Get(server.URL, opts) 1180 | if err != nil { 1181 | t.Fatalf("Did not expect a transport error, got: %v", err) 1182 | } 1183 | if resp.StatusCode != http.StatusUnauthorized { 1184 | t.Fatalf("Expected 401, got %d", resp.StatusCode) 1185 | } 1186 | if !bytes.Contains(resp.Body, []byte("Invalid credentials")) { 1187 | t.Errorf("Expected response body to contain 'Invalid credentials', got %s", resp.Body) 1188 | } 1189 | }) 1190 | 1191 | t.Run("Missing Credentials", func(t *testing.T) { 1192 | resp, err := Get(server.URL) 1193 | if err != nil { 1194 | t.Fatalf("Unexpected error: %v", err) 1195 | } 1196 | if resp.StatusCode != http.StatusUnauthorized { 1197 | t.Fatalf("Expected 401, got %d", resp.StatusCode) 1198 | } 1199 | if !bytes.Contains(resp.Body, []byte("Missing Authorization header")) { 1200 | t.Errorf("Expected response body to contain 'Missing Authorization header', got %s", resp.Body) 1201 | } 1202 | }) 1203 | } 1204 | 1205 | func TestParams(t *testing.T) { 1206 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1207 | w.WriteHeader(http.StatusOK) 1208 | w.Write([]byte(r.URL.RawQuery)) 1209 | })) 1210 | defer server.Close() 1211 | 1212 | t.Run("Single Param", func(t *testing.T) { 1213 | opts := &RequestOptions{ 1214 | Params: map[string]string{ 1215 | "foo": "bar", 1216 | }, 1217 | } 1218 | resp, err := Get(server.URL, opts) 1219 | if err != nil { 1220 | t.Fatalf("Expected no error, got %v", err) 1221 | } 1222 | if resp.StatusCode != http.StatusOK { 1223 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1224 | } 1225 | 1226 | if string(resp.Body) != "foo=bar" { 1227 | t.Errorf("Expected 'foo=bar', got %q", resp.Body) 1228 | } 1229 | }) 1230 | 1231 | t.Run("Multiple Params", func(t *testing.T) { 1232 | opts := &RequestOptions{ 1233 | Params: map[string]string{ 1234 | "param1": "value1", 1235 | "param2": "value2", 1236 | }, 1237 | } 1238 | resp, err := Get(server.URL, opts) 1239 | if err != nil { 1240 | t.Fatalf("Expected no error, got %v", err) 1241 | } 1242 | if resp.StatusCode != http.StatusOK { 1243 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1244 | } 1245 | 1246 | rawQuery := string(resp.Body) 1247 | if !strings.Contains(rawQuery, "param1=value1") || 1248 | !strings.Contains(rawQuery, "param2=value2") { 1249 | t.Errorf("Expected query to contain param1=value1 and param2=value2, got %q", rawQuery) 1250 | } 1251 | }) 1252 | 1253 | t.Run("Special Characters", func(t *testing.T) { 1254 | opts := &RequestOptions{ 1255 | Params: map[string]string{ 1256 | "q": "hello world", 1257 | }, 1258 | } 1259 | resp, err := Get(server.URL, opts) 1260 | if err != nil { 1261 | t.Fatalf("Error: %v", err) 1262 | } 1263 | if resp.StatusCode != http.StatusOK { 1264 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1265 | } 1266 | rawQuery := string(resp.Body) 1267 | if !strings.Contains(rawQuery, "q=hello") { 1268 | t.Errorf("Expected query to contain 'q=hello', got %q", rawQuery) 1269 | } 1270 | }) 1271 | } 1272 | 1273 | func TestMaxBodyAndContentLength(t *testing.T) { 1274 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1275 | io.Copy(io.Discard, r.Body) 1276 | 1277 | sizeStr := r.URL.Query().Get("size") 1278 | if sizeStr == "" { 1279 | sizeStr = "100" 1280 | } 1281 | size, err := strconv.Atoi(sizeStr) 1282 | if err != nil { 1283 | size = 100 1284 | } 1285 | 1286 | data := bytes.Repeat([]byte("a"), size) 1287 | w.WriteHeader(http.StatusOK) 1288 | w.Write(data) 1289 | })) 1290 | defer server.Close() 1291 | 1292 | t.Run("RequestBodyExceedsMax", func(t *testing.T) { 1293 | body := bytes.Repeat([]byte("x"), 3000) 1294 | opts := &RequestOptions{ 1295 | Method: "POST", 1296 | MaxBodyLength: 2000, 1297 | Body: body, 1298 | } 1299 | resp, err := Request("POST", server.URL, opts) 1300 | if err == nil { 1301 | t.Fatalf("Expected error due to exceeding MaxBodyLength, got success: %+v", resp) 1302 | } 1303 | t.Logf("RequestBodyExceedsMax: got error as expected: %v", err) 1304 | }) 1305 | 1306 | t.Run("RequestBodyWithinMax", func(t *testing.T) { 1307 | body := bytes.Repeat([]byte("x"), 1000) 1308 | opts := &RequestOptions{ 1309 | Method: "POST", 1310 | MaxBodyLength: 2000, 1311 | Body: body, 1312 | } 1313 | resp, err := Request("POST", server.URL, opts) 1314 | if err != nil { 1315 | t.Fatalf("Did not expect error, got %v", err) 1316 | } 1317 | if resp.StatusCode != http.StatusOK { 1318 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1319 | } 1320 | }) 1321 | 1322 | t.Run("ResponseExceedsMax", func(t *testing.T) { 1323 | opts := &RequestOptions{ 1324 | MaxContentLength: 2000, 1325 | } 1326 | urlWithSize := server.URL + "?size=3000" 1327 | resp, err := Get(urlWithSize, opts) 1328 | if err == nil { 1329 | t.Fatalf("Expected error due to exceeding MaxContentLength, got success: %+v", resp) 1330 | } 1331 | t.Logf("ResponseExceedsMax: got error as expected: %v", err) 1332 | }) 1333 | 1334 | t.Run("ResponseWithinMax", func(t *testing.T) { 1335 | opts := &RequestOptions{ 1336 | MaxContentLength: 2000, 1337 | } 1338 | urlWithSize := server.URL + "?size=1000" 1339 | resp, err := Get(urlWithSize, opts) 1340 | if err != nil { 1341 | t.Fatalf("Did not expect error, got %v", err) 1342 | } 1343 | if resp.StatusCode != http.StatusOK { 1344 | t.Fatalf("Expected 200, got %d", resp.StatusCode) 1345 | } 1346 | if len(resp.Body) != 1000 { 1347 | t.Fatalf("Expected 1000 bytes, got %d", len(resp.Body)) 1348 | } 1349 | }) 1350 | } 1351 | 1352 | func TestInterceptorErrorHandling(t *testing.T) { 1353 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1354 | w.WriteHeader(http.StatusOK) 1355 | w.Write([]byte(`{"message": "ok"}`)) 1356 | })) 1357 | defer server.Close() 1358 | 1359 | t.Run("RequestInterceptorError", func(t *testing.T) { 1360 | failingRequestInterceptor := func(req *http.Request) error { 1361 | return fmt.Errorf("request interceptor forced error") 1362 | } 1363 | 1364 | opts := &RequestOptions{ 1365 | InterceptorOptions: InterceptorOptions{ 1366 | RequestInterceptors: []func(*http.Request) error{ 1367 | failingRequestInterceptor, 1368 | }, 1369 | }, 1370 | } 1371 | 1372 | resp, err := Get(server.URL, opts) 1373 | if err == nil { 1374 | t.Fatalf("Expected an error from request interceptor, got response: %+v", resp) 1375 | } 1376 | if !strings.Contains(err.Error(), "request interceptor forced error") { 1377 | t.Errorf("Expected error to contain 'request interceptor forced error', got: %v", err) 1378 | } 1379 | }) 1380 | 1381 | t.Run("ResponseInterceptorError", func(t *testing.T) { 1382 | failingResponseInterceptor := func(resp *http.Response) error { 1383 | return fmt.Errorf("response interceptor forced error") 1384 | } 1385 | 1386 | opts := &RequestOptions{ 1387 | InterceptorOptions: InterceptorOptions{ 1388 | ResponseInterceptors: []func(*http.Response) error{ 1389 | failingResponseInterceptor, 1390 | }, 1391 | }, 1392 | } 1393 | 1394 | resp, err := Get(server.URL, opts) 1395 | if err == nil { 1396 | t.Fatalf("Expected an error from response interceptor, got response: %+v", resp) 1397 | } 1398 | if !strings.Contains(err.Error(), "response interceptor forced error") { 1399 | t.Errorf("Expected error to contain 'response interceptor forced error', got: %v", err) 1400 | } 1401 | }) 1402 | } 1403 | 1404 | func TestInvalidProxy(t *testing.T) { 1405 | opts := &RequestOptions{ 1406 | Proxy: &Proxy{ 1407 | Protocol: "invalid-protocol", 1408 | Host: "bad_host", 1409 | Port: 99999, // invalid port 1410 | }, 1411 | } 1412 | 1413 | _, err := Get("http://example.com", opts) 1414 | if err == nil { 1415 | t.Fatal("Expected error due to invalid proxy settings, got nil") 1416 | } 1417 | 1418 | t.Logf("InvalidProxy test got expected error: %v", err) 1419 | } 1420 | 1421 | func TestInvalidMethod(t *testing.T) { 1422 | opts := &RequestOptions{Method: "INVALID_METHOD!"} 1423 | resp, err := Request("", "http://example.com", opts) 1424 | if err == nil { 1425 | t.Fatalf("Expected error for invalid method, got response: %v", resp) 1426 | } 1427 | if !strings.Contains(err.Error(), "invalid HTTP method") { 1428 | t.Errorf("Unexpected error: %v", err) 1429 | } 1430 | } 1431 | 1432 | func TestEmptyURL(t *testing.T) { 1433 | opts := &RequestOptions{} 1434 | 1435 | resp, err := Get("", opts) 1436 | if err == nil { 1437 | t.Fatalf("Expected error for empty URL, but got response: %v", resp) 1438 | } 1439 | 1440 | t.Logf("EmptyURL test got expected error: %v", err) 1441 | } 1442 | 1443 | func TestNonHTTPBaseURL(t *testing.T) { 1444 | client := NewClient("ftp://some.ftp.site") 1445 | 1446 | resp, err := client.Request(&RequestOptions{ 1447 | Method: "GET", 1448 | URL: "/test", 1449 | }) 1450 | if err == nil { 1451 | t.Fatalf("Expected error for non-HTTP base URL, got response: %v", resp) 1452 | } 1453 | 1454 | t.Logf("NonHTTP_BaseURL test got expected error: %v", err) 1455 | } 1456 | --------------------------------------------------------------------------------