Native Routes (Go)#
route.go is an API route written in Go. URLs map the same way as route.php; only the handler compiles to a native binary. Declare func GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS in the standard http.HandlerFunc shape, with no main() or framework import. It suits CPU-heavy endpoints.
// src/Pages/api/hello/route.go
package main
import (
"encoding/json"
"net/http"
)
func GET(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "Hello from Go!"})
}vendor/bin/relayer native:build # once per route.go change
curl -i http://localhost:8080/api/helloBuilding#
relayer native:build compiles every route.go under src/Pages and the root middleware.go to var/cache/native/bin/<slug>. <slug> is the URL pattern with / replaced by __ (/api/users/[id] → api__users__[id]).
- It needs
goonPATH(orGO_BIN=/path/to/go), which is why it is separate fromrelayer compile. - A
go.mod/go.sumnext toroute.gois used, so third-party modules work. Onlyroute.goandgo.mod/go.sumare copied, so a neighboringhelper.gois not compiled; import shared code as a module. - Two routes with the same slug, a
route.gowith no handler, and one that is notpackage mainare build errors. - Nothing rebuilds during a request. An unbuilt route answers a JSON
500pointing atnative:build, and in dev so does a binary older than itsroute.go.
The scaffolded Dockerfile has no Go and does not run native:build. In that production image every route.go is an unbuilt 500, so run native:build in a build stage with Go and copy var/cache/native/bin to the final image.
Request flow#
There is no long-running Go server. PHP starts the binary as a CGI child process per request (proc_open), with a few milliseconds of overhead per call. The handler sees:
r.Method,r.URL,r.Header,r.Host,r.RemoteAddr,r.Body. A client'sProxy:header is not forwarded, and the port inr.RemoteAddris0.- Dynamic segments as JSON in
RELAYER_ROUTE_PARAMS, and the pattern inRELAYER_ROUTE_PATTERN.
var params map[string]string
json.Unmarshal([]byte(os.Getenv("RELAYER_ROUTE_PARAMS")), ¶ms)
id := params["id"]- The PHP process's real environment variables, but not
.envvalues. Pass settings Go needs as real environment variables from the shell or container. - A
multipart/form-databody rebuilt from what PHP parsed, sor.FormFileworks. An upload PHP refused becomes a JSON500.
Methods and failures#
Methods follow route.php: an undeclared OPTIONS is 204 + Allow, HEAD with a GET runs GET and drops the body, and any other undeclared method is 405 + Allow.
A non-zero exit (including panic), output that is not a CGI response, and exceeding the timeout all become a JSON 500. stderr goes to the PSR-3 logger (warning for a successful run). The timeout defaults to 30 seconds and NATIVE_ROUTE_TIMEOUT changes it. It does not work on a PHP with proc_open in disable_functions.
What does not apply#
route.go has no autowiring, PageContext, Request, #[Auth] or requireAuth(); protect it with the root middleware. _private folders exclude route.go too, and a directory holds one handler (route.go next to route.php or a page is a scan error).
middleware.go#
The root middleware can be written in Go too. Write one func Proxy in src/Pages/middleware.go; native:build builds it to var/cache/native/bin/middleware, and it starts per request before route matching.
// src/Pages/middleware.go
package main
import "net/http"
func Proxy(w http.ResponseWriter, r *http.Request) {
// Short-circuit: without Relayer-Next, this is the final response
if r.Header.Get("X-Api-Key") == "" && len(r.URL.Path) >= 4 && r.URL.Path[:4] == "/api" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(401)
w.Write([]byte(`{"error":"missing api key"}`))
return
}
// Continue: with Relayer-Next, the matched page / route runs
w.Header().Set("Relayer-Next", "1")
w.Header().Set("Relayer-Request-Header-X-User", "42") // request header the route sees
w.Header().Set("X-Proxied-By", "go") // response header merged into the final response
}- With
Relayer-Next: 1→ continue; the body is ignored.Relayer-Rewrite-Path: /otherroutes to another path, andRelayer-Request-Header-<Name>: valueadds or overwrites a request header. Other response headers merge into the final response (exceptStatus:,Content-TypeandContent-Length). - Without
Relayer-Next→ short-circuit:Status:(default 200), headers and body go out as is. Relayer-*headers never reach the client.RELAYER_MATCHED_PATTERNholds the pattern the path would match (empty for a 404).- Having both
middleware.phpandmiddleware.gothrowsRuntimeException. - A failure is a JSON
500, even for a page request. - Every page and route pays a few milliseconds.
/_defer/*and/_profilerrun outside it, so checks that must cover them belong in the guard.
Revisions (1)
- v0.31.0: 新規ページ