Skip to content

Commit 83dccd7

Browse files
authored
Merge pull request #8 from jsr-probitas/feature/add-http-testing-endpoints
feat(echo-http): Add comprehensive HTTP testing endpoints
2 parents 6427067 + ac58768 commit 83dccd7

20 files changed

+2362
-12
lines changed

echo-http/README.md

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,69 @@ docker run -p 8080:8080 -v $(pwd)/.env:/app/.env ghcr.io/jsr-probitas/echo-http:
3434

3535
## API
3636

37-
| Endpoint | Method | Description |
38-
| ------------------ | ------ | ----------------------------------------- |
39-
| `/get` | GET | Echo request info (query params, headers) |
40-
| `/post` | POST | Echo request body (JSON, form data) |
41-
| `/put` | PUT | Echo request body |
42-
| `/patch` | PATCH | Echo request body |
43-
| `/delete` | DELETE | Echo request info |
44-
| `/headers` | GET | Echo headers only |
45-
| `/status/{code}` | GET | Return specified status code (100-599) |
46-
| `/delay/{seconds}` | GET | Echo after delay |
47-
| `/health` | GET | Health check |
37+
### Echo Endpoints
38+
39+
| Endpoint | Method | Description |
40+
| ------------ | ------ | ----------------------------------------- |
41+
| `/get` | GET | Echo request info (query params, headers) |
42+
| `/post` | POST | Echo request body (JSON, form data) |
43+
| `/put` | PUT | Echo request body |
44+
| `/patch` | PATCH | Echo request body |
45+
| `/delete` | DELETE | Echo request info |
46+
| `/anything` | ANY | Echo any request (method, headers, body) |
47+
| `/anything/*`| ANY | Echo any request with path |
48+
49+
### Utility Endpoints
50+
51+
| Endpoint | Method | Description |
52+
| ------------------ | ------ | -------------------------------------- |
53+
| `/headers` | GET | Echo headers only |
54+
| `/ip` | GET | Return client IP address |
55+
| `/user-agent` | GET | Return User-Agent header |
56+
| `/status/{code}` | ANY | Return specified status code (100-599) |
57+
| `/delay/{seconds}` | GET | Echo after delay (max 30s) |
58+
| `/health` | GET | Health check |
59+
60+
### Redirect Endpoints
61+
62+
| Endpoint | Method | Description |
63+
| ---------------------- | ------ | ------------------------------------- |
64+
| `/redirect/{n}` | GET | Redirect n times before final response|
65+
| `/redirect-to` | GET | Redirect to URL (?url=...&status_code=)|
66+
| `/absolute-redirect/{n}`| GET | Redirect n times with absolute URLs |
67+
| `/relative-redirect/{n}`| GET | Redirect n times with relative URLs |
68+
69+
### Authentication Endpoints
70+
71+
| Endpoint | Method | Description |
72+
| ---------------------------- | ------ | ---------------------------------------- |
73+
| `/basic-auth/{user}/{pass}` | GET | Basic auth (200 if match, 401 otherwise) |
74+
| `/hidden-basic-auth/{user}/{pass}` | GET | Basic auth (200 if match, 404 otherwise)|
75+
| `/bearer` | GET | Bearer token validation |
76+
77+
### Cookie Endpoints
78+
79+
| Endpoint | Method | Description |
80+
| ----------------- | ------ | ---------------------------------------- |
81+
| `/cookies` | GET | Echo request cookies |
82+
| `/cookies/set` | GET | Set cookies (?name=value) and redirect |
83+
| `/cookies/delete` | GET | Delete cookies (?name) and redirect |
84+
85+
### Binary Data Endpoints
86+
87+
| Endpoint | Method | Description |
88+
| -------------- | ------ | -------------------------------- |
89+
| `/bytes/{n}` | GET | Return n random bytes (max 100KB)|
90+
| `/stream/{n}` | GET | Stream n JSON lines (max 100) |
91+
| `/drip` | GET | Drip data (?duration=&numbytes=&delay=)|
92+
93+
### Compression Endpoints
94+
95+
| Endpoint | Method | Description |
96+
| ----------- | ------ | ----------------------------- |
97+
| `/gzip` | GET | Return gzip-compressed response |
98+
| `/deflate` | GET | Return deflate-compressed response |
99+
| `/brotli` | GET | Return brotli-compressed response |
48100

49101
See [docs/api.md](./docs/api.md) for detailed API reference.
50102

@@ -78,6 +130,32 @@ curl http://localhost:8080/status/418
78130

79131
# Delayed response (for timeout testing)
80132
curl http://localhost:8080/delay/5
133+
134+
# Redirect testing
135+
curl -L http://localhost:8080/redirect/3
136+
137+
# Basic authentication
138+
curl -u user:pass http://localhost:8080/basic-auth/user/pass
139+
140+
# Bearer token
141+
curl -H "Authorization: Bearer my-token" http://localhost:8080/bearer
142+
143+
# Cookie handling
144+
curl -c cookies.txt http://localhost:8080/cookies/set?session=abc123
145+
curl -b cookies.txt http://localhost:8080/cookies
146+
147+
# Get client IP
148+
curl http://localhost:8080/ip
149+
150+
# Compression testing
151+
curl --compressed http://localhost:8080/gzip
152+
curl --compressed http://localhost:8080/brotli
153+
154+
# Stream data
155+
curl http://localhost:8080/stream/5
156+
157+
# Random bytes
158+
curl http://localhost:8080/bytes/100 --output random.bin
81159
```
82160

83161
## Development

echo-http/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/jsr-probitas/dockerfiles/echo-http
33
go 1.23
44

55
require (
6+
github.com/andybalholm/brotli v1.1.1
67
github.com/go-chi/chi/v5 v5.2.3
78
github.com/joho/godotenv v1.5.1
89
)

echo-http/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
2+
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
13
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
24
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
35
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
46
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
7+
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
8+
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=

echo-http/handlers/anything.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
"strings"
8+
)
9+
10+
type AnythingResponse struct {
11+
Method string `json:"method"`
12+
URL string `json:"url"`
13+
Args map[string]string `json:"args"`
14+
Headers map[string]string `json:"headers"`
15+
Origin string `json:"origin"`
16+
Data string `json:"data,omitempty"`
17+
JSON any `json:"json,omitempty"`
18+
Form map[string]string `json:"form,omitempty"`
19+
Files map[string]string `json:"files,omitempty"`
20+
}
21+
22+
// AnythingHandler echoes any request information.
23+
// ANY /anything - Echo any request (method, headers, body, etc.)
24+
// ANY /anything/{path} - Echo any request with path
25+
func AnythingHandler(w http.ResponseWriter, r *http.Request) {
26+
response := AnythingResponse{
27+
Method: r.Method,
28+
URL: r.URL.RequestURI(),
29+
Args: make(map[string]string),
30+
Headers: make(map[string]string),
31+
Origin: getClientIP(r),
32+
}
33+
34+
for key, values := range r.URL.Query() {
35+
if len(values) > 0 {
36+
response.Args[key] = values[0]
37+
}
38+
}
39+
40+
for key, values := range r.Header {
41+
if len(values) > 0 {
42+
response.Headers[key] = values[0]
43+
}
44+
}
45+
46+
if r.Method != http.MethodGet && r.Method != http.MethodHead && r.Method != http.MethodDelete {
47+
body, err := io.ReadAll(r.Body)
48+
if err == nil && len(body) > 0 {
49+
response.Data = string(body)
50+
51+
contentType := r.Header.Get("Content-Type")
52+
if strings.Contains(contentType, "application/json") {
53+
var jsonData any
54+
if err := json.Unmarshal(body, &jsonData); err == nil {
55+
response.JSON = jsonData
56+
}
57+
} else if strings.Contains(contentType, "application/x-www-form-urlencoded") {
58+
r.Body = io.NopCloser(strings.NewReader(string(body)))
59+
if err := r.ParseForm(); err == nil {
60+
formData := make(map[string]string)
61+
for key, values := range r.PostForm {
62+
if len(values) > 0 {
63+
formData[key] = values[0]
64+
}
65+
}
66+
response.Form = formData
67+
}
68+
} else if strings.Contains(contentType, "multipart/form-data") {
69+
r.Body = io.NopCloser(strings.NewReader(string(body)))
70+
if err := r.ParseMultipartForm(32 << 20); err == nil {
71+
if r.MultipartForm != nil {
72+
formData := make(map[string]string)
73+
for key, values := range r.MultipartForm.Value {
74+
if len(values) > 0 {
75+
formData[key] = values[0]
76+
}
77+
}
78+
response.Form = formData
79+
80+
files := make(map[string]string)
81+
for key, fileHeaders := range r.MultipartForm.File {
82+
if len(fileHeaders) > 0 {
83+
files[key] = fileHeaders[0].Filename
84+
}
85+
}
86+
response.Files = files
87+
}
88+
}
89+
}
90+
}
91+
}
92+
93+
w.Header().Set("Content-Type", "application/json")
94+
_ = json.NewEncoder(w).Encode(response)
95+
}

0 commit comments

Comments
 (0)