diff --git a/Context.go b/Context.go
index 46b3b8c..7afb303 100644
--- a/Context.go
+++ b/Context.go
@@ -9,6 +9,7 @@ type Context interface {
Bytes([]byte) error
Error(...any) error
Next() error
+ Redirect(int, string) error
Request() Request
Response() Response
Status(int) Context
@@ -51,6 +52,14 @@ func (ctx *context) Next() error {
return ctx.server.handlers[ctx.handlerCount](ctx)
}
+// Redirect redirects the client to a different location
+// with the specified status code.
+func (ctx *context) Redirect(status int, location string) error {
+ ctx.response.SetStatus(status)
+ ctx.response.SetHeader("Location", location)
+ return nil
+}
+
// Request returns the HTTP request.
func (ctx *context) Request() Request {
return &ctx.request
diff --git a/Context_test.go b/Context_test.go
index 05bc24f..b3a6005 100644
--- a/Context_test.go
+++ b/Context_test.go
@@ -4,8 +4,8 @@ import (
"errors"
"testing"
- "git.akyoto.dev/go/assert"
- "git.akyoto.dev/go/web"
+ "git.urbach.dev/go/assert"
+ "git.urbach.dev/go/web"
)
func TestBytes(t *testing.T) {
@@ -55,3 +55,15 @@ func TestErrorMultiple(t *testing.T) {
assert.Equal(t, response.Status(), 401)
assert.Equal(t, string(response.Body()), "")
}
+
+func TestRedirect(t *testing.T) {
+ s := web.NewServer()
+
+ s.Get("/", func(ctx web.Context) error {
+ return ctx.Redirect(301, "/target")
+ })
+
+ response := s.Request("GET", "/", nil, nil)
+ assert.Equal(t, response.Status(), 301)
+ assert.Equal(t, response.Header("Location"), "/target")
+}
diff --git a/README.md b/README.md
index 9b912f2..149eb93 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# web
-A fast HTTP/1.1 web server that can sit behind a reverse proxy like `caddy` or `nginx` for HTTP 1/2/3 support.
+A minimal HTTP/1.1 web server that sits behind a reverse proxy like `caddy` or `nginx` for HTTP 1/2/3 support.
## Features
@@ -11,7 +11,7 @@ A fast HTTP/1.1 web server that can sit behind a reverse proxy like `caddy` or `
## Installation
```shell
-go get git.akyoto.dev/go/web
+go get git.urbach.dev/go/web
```
## Usage
@@ -26,12 +26,23 @@ s.Get("/", func(ctx web.Context) error {
// Parameter route
s.Get("/blog/:post", func(ctx web.Context) error {
- return ctx.String(ctx.Get("post"))
+ return ctx.String(ctx.Request().Param("post"))
})
// Wildcard route
s.Get("/images/*file", func(ctx web.Context) error {
- return ctx.String(ctx.Get("file"))
+ return ctx.String(ctx.Request().Param("file"))
+})
+
+// Middleware
+s.Use(func(ctx web.Context) error {
+ start := time.Now()
+
+ defer func() {
+ fmt.Println(ctx.Request().Path(), time.Since(start))
+ }()
+
+ return ctx.Next()
})
s.Run(":8080")
@@ -44,6 +55,7 @@ PASS: TestBytes
PASS: TestString
PASS: TestError
PASS: TestErrorMultiple
+PASS: TestRedirect
PASS: TestRequest
PASS: TestRequestHeader
PASS: TestRequestParam
@@ -57,7 +69,9 @@ PASS: TestRun
PASS: TestBadRequest
PASS: TestBadRequestHeader
PASS: TestBadRequestMethod
+PASS: TestBadRequestPath
PASS: TestBadRequestProtocol
+PASS: TestConnectionClose
PASS: TestEarlyClose
PASS: TestUnavailablePort
coverage: 100.0% of statements
@@ -69,7 +83,7 @@ coverage: 100.0% of statements
## License
-Please see the [license documentation](https://akyoto.dev/license).
+Please see the [license documentation](https://urbach.dev/license).
## Copyright
diff --git a/Request.go b/Request.go
index 9ec8ef9..6c7ef3c 100644
--- a/Request.go
+++ b/Request.go
@@ -3,7 +3,7 @@ package web
import (
"bufio"
- "git.akyoto.dev/go/router"
+ "git.urbach.dev/go/router"
)
// Request is an interface for HTTP requests.
diff --git a/Request_test.go b/Request_test.go
index 6a08fd0..1e079a8 100644
--- a/Request_test.go
+++ b/Request_test.go
@@ -4,8 +4,8 @@ import (
"fmt"
"testing"
- "git.akyoto.dev/go/assert"
- "git.akyoto.dev/go/web"
+ "git.urbach.dev/go/assert"
+ "git.urbach.dev/go/web"
)
func TestRequest(t *testing.T) {
diff --git a/Response_test.go b/Response_test.go
index dc62965..9028acf 100644
--- a/Response_test.go
+++ b/Response_test.go
@@ -6,8 +6,8 @@ import (
"io"
"testing"
- "git.akyoto.dev/go/assert"
- "git.akyoto.dev/go/web"
+ "git.urbach.dev/go/assert"
+ "git.urbach.dev/go/web"
)
func TestWrite(t *testing.T) {
diff --git a/Server.go b/Server.go
index f987c51..ba163a0 100644
--- a/Server.go
+++ b/Server.go
@@ -13,7 +13,7 @@ import (
"sync"
"syscall"
- "git.akyoto.dev/go/router"
+ "git.urbach.dev/go/router"
)
// Server is the interface for an HTTP server.
@@ -121,6 +121,7 @@ func (s *server) handleConnection(conn net.Conn) {
ctx = s.contextPool.Get().(*context)
method string
url string
+ close bool
)
ctx.reader.Reset(conn)
@@ -128,7 +129,7 @@ func (s *server) handleConnection(conn net.Conn) {
defer conn.Close()
defer s.contextPool.Put(ctx)
- for {
+ for !close {
// Read the HTTP request line
message, err := ctx.reader.ReadString('\n')
@@ -156,7 +157,14 @@ func (s *server) handleConnection(conn net.Conn) {
lastSpace = len(message) - len("\r\n")
}
- url = message[space+1 : lastSpace]
+ space += 1
+
+ if space > lastSpace {
+ io.WriteString(conn, "HTTP/1.1 400 Bad Request\r\n\r\n")
+ return
+ }
+
+ url = message[space:lastSpace]
// Add headers until we meet an empty line
for {
@@ -176,6 +184,10 @@ func (s *server) handleConnection(conn net.Conn) {
continue
}
+ if colon > len(message)-4 {
+ continue
+ }
+
key := message[:colon]
value := message[colon+2 : len(message)-2]
@@ -183,6 +195,10 @@ func (s *server) handleConnection(conn net.Conn) {
Key: key,
Value: value,
})
+
+ if value == "close" && strings.EqualFold(key, "connection") {
+ close = true
+ }
}
// Handle the request
diff --git a/Server_test.go b/Server_test.go
index 9793aef..f2c142b 100644
--- a/Server_test.go
+++ b/Server_test.go
@@ -7,8 +7,8 @@ import (
"syscall"
"testing"
- "git.akyoto.dev/go/assert"
- "git.akyoto.dev/go/web"
+ "git.urbach.dev/go/assert"
+ "git.urbach.dev/go/web"
)
func TestPanic(t *testing.T) {
@@ -77,7 +77,7 @@ func TestBadRequestHeader(t *testing.T) {
assert.Nil(t, err)
defer conn.Close()
- _, err = io.WriteString(conn, "GET / HTTP/1.1\r\nBadHeader\r\nGood: Header\r\n\r\n")
+ _, err = io.WriteString(conn, "GET / HTTP/1.1\r\nBad\r\nBad:\r\nGood: Header\r\n\r\n")
assert.Nil(t, err)
buffer := make([]byte, len("HTTP/1.1 200"))
@@ -110,6 +110,27 @@ func TestBadRequestMethod(t *testing.T) {
s.Run(":8080")
}
+func TestBadRequestPath(t *testing.T) {
+ s := web.NewServer()
+
+ go func() {
+ defer syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
+
+ conn, err := net.Dial("tcp", ":8080")
+ assert.Nil(t, err)
+ defer conn.Close()
+
+ _, err = io.WriteString(conn, "GET \n")
+ assert.Nil(t, err)
+
+ response, err := io.ReadAll(conn)
+ assert.Nil(t, err)
+ assert.Equal(t, string(response), "HTTP/1.1 400 Bad Request\r\n\r\n")
+ }()
+
+ s.Run(":8080")
+}
+
func TestBadRequestProtocol(t *testing.T) {
s := web.NewServer()
@@ -136,6 +157,26 @@ func TestBadRequestProtocol(t *testing.T) {
s.Run(":8080")
}
+func TestConnectionClose(t *testing.T) {
+ s := web.NewServer()
+
+ go func() {
+ defer syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
+
+ conn, err := net.Dial("tcp", ":8080")
+ assert.Nil(t, err)
+ defer conn.Close()
+
+ _, err = io.WriteString(conn, "GET / HTTP/1.1\r\nConnection: close\r\n\r\n")
+ assert.Nil(t, err)
+
+ _, err = io.ReadAll(conn)
+ assert.Nil(t, err)
+ }()
+
+ s.Run(":8080")
+}
+
func TestEarlyClose(t *testing.T) {
s := web.NewServer()
diff --git a/examples/hello/main.go b/examples/hello/main.go
index c9853da..b42db84 100644
--- a/examples/hello/main.go
+++ b/examples/hello/main.go
@@ -1,7 +1,7 @@
package main
import (
- "git.akyoto.dev/go/web"
+ "git.urbach.dev/go/web"
)
func main() {
diff --git a/go.mod b/go.mod
index a4d4a7c..f2bf46e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
-module git.akyoto.dev/go/web
+module git.urbach.dev/go/web
-go 1.22
+go 1.24
require (
- git.akyoto.dev/go/assert v0.1.3
- git.akyoto.dev/go/router v0.1.4
+ git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf
+ git.urbach.dev/go/router v0.0.0-20250601162231-e35d5715d1a5
)
diff --git a/go.sum b/go.sum
index a12e1ec..4c3db0c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,4 @@
-git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8=
-git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM=
-git.akyoto.dev/go/router v0.1.4 h1:ZL5HPl4aNn4QKihf3VVs0Mm9R6ZGn2StAHGRQxjEbws=
-git.akyoto.dev/go/router v0.1.4/go.mod h1:rbHbkLJlQOafuOuvBalO3O8E0JtMFPT3zzTKX3h9T08=
+git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf h1:BQWa5GKNUsA5CSUa/+UlFWYCEVe3IDDKRbVqBLK0mAE=
+git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf/go.mod h1:y9jGII9JFiF1HNIju0u87OyPCt82xKCtqnAFyEreCDo=
+git.urbach.dev/go/router v0.0.0-20250601162231-e35d5715d1a5 h1:3qlZjgWbrHw4LWM4uBzOTldbpqCLJdeSgvcYK6f3xpc=
+git.urbach.dev/go/router v0.0.0-20250601162231-e35d5715d1a5/go.mod h1:O+doTe0DZdT2XMsTY5pe6qUMqEEIAbwtX6Xp6EqHw34=
diff --git a/content/content.go b/send/send.go
similarity index 97%
rename from content/content.go
rename to send/send.go
index 2eca461..9c4c2ca 100644
--- a/content/content.go
+++ b/send/send.go
@@ -1,9 +1,9 @@
-package content
+package send
import (
"encoding/json"
- "git.akyoto.dev/go/web"
+ "git.urbach.dev/go/web"
)
// CSS sends the body with the content type set to `text/css`.
diff --git a/content/content_test.go b/send/send_test.go
similarity index 79%
rename from content/content_test.go
rename to send/send_test.go
index 5d4d2ca..fec22c3 100644
--- a/content/content_test.go
+++ b/send/send_test.go
@@ -1,42 +1,42 @@
-package content_test
+package send_test
import (
"testing"
- "git.akyoto.dev/go/assert"
- "git.akyoto.dev/go/web"
- "git.akyoto.dev/go/web/content"
+ "git.urbach.dev/go/assert"
+ "git.urbach.dev/go/web"
+ "git.urbach.dev/go/web/send"
)
func TestContentTypes(t *testing.T) {
s := web.NewServer()
s.Get("/css", func(ctx web.Context) error {
- return content.CSS(ctx, "body{}")
+ return send.CSS(ctx, "body{}")
})
s.Get("/csv", func(ctx web.Context) error {
- return content.CSV(ctx, "ID;Name\n")
+ return send.CSV(ctx, "ID;Name\n")
})
s.Get("/html", func(ctx web.Context) error {
- return content.HTML(ctx, "")
+ return send.HTML(ctx, "")
})
s.Get("/js", func(ctx web.Context) error {
- return content.JS(ctx, "console.log(42)")
+ return send.JS(ctx, "console.log(42)")
})
s.Get("/json", func(ctx web.Context) error {
- return content.JSON(ctx, struct{ Name string }{Name: "User 1"})
+ return send.JSON(ctx, struct{ Name string }{Name: "User 1"})
})
s.Get("/text", func(ctx web.Context) error {
- return content.Text(ctx, "Hello")
+ return send.Text(ctx, "Hello")
})
s.Get("/xml", func(ctx web.Context) error {
- return content.XML(ctx, "")
+ return send.XML(ctx, "")
})
tests := []struct {