forked from bots-garden/ori
| ✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme | 1 | // Package health probes an ori server's GET /healthz endpoint. It is pure Go |
| 2 | // (no GUI dependency) so the connection logic of the desktop client can be | |
| 3 | // unit-tested against an httptest server. | |
| 4 | package health | |
| 5 | ||
| 6 | import ( | |
| 7 | "context" | |
| 8 | "encoding/json" | |
| 9 | "fmt" | |
| 10 | "io" | |
| 11 | "net/http" | |
| 12 | "time" | |
| 13 | ) | |
| 14 | ||
| 15 | // DefaultTimeout bounds a single probe; a desktop connection screen must fail | |
| 16 | // fast when the server is down. | |
| 17 | const DefaultTimeout = 3 * time.Second | |
| 18 | ||
| 19 | // Report is the outcome of one probe, shaped to cross the Wails bridge as a | |
| 20 | // plain JSON object (no error type, so the JavaScript side never has to | |
| 21 | // distinguish a rejected promise from a negative answer). | |
| 22 | type Report struct { | |
| 23 | // OK is true when /healthz answered with a 2xx status. | |
| 24 | OK bool `json:"ok"` | |
| 25 | // URL is the exact URL that was probed. | |
| 26 | URL string `json:"url"` | |
| 27 | // StatusCode is the HTTP status received, or 0 when no response arrived. | |
| 28 | StatusCode int `json:"statusCode"` | |
| 29 | // Detail is a short human-readable explanation ("ori is reachable", | |
| 30 | // the transport error, or the unexpected status). | |
| 31 | Detail string `json:"detail"` | |
| 32 | } | |
| 33 | ||
| 34 | // Check performs GET <baseURL>/healthz. baseURL must already be normalised | |
| 35 | // (scheme + host, no trailing slash). A nil client uses http.DefaultTransport | |
| 36 | // with DefaultTimeout. | |
| 37 | func Check(ctx context.Context, client *http.Client, baseURL string) Report { | |
| 38 | if client == nil { | |
| 39 | client = &http.Client{Timeout: DefaultTimeout} | |
| 40 | } | |
| 41 | target := baseURL + "/healthz" | |
| 42 | rep := Report{URL: target} | |
| 43 | ||
| 44 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil) | |
| 45 | if err != nil { | |
| 46 | rep.Detail = fmt.Sprintf("invalid URL: %v", err) | |
| 47 | return rep | |
| 48 | } | |
| 49 | resp, err := client.Do(req) | |
| 50 | if err != nil { | |
| 51 | rep.Detail = fmt.Sprintf("cannot reach ori: %v", err) | |
| 52 | return rep | |
| 53 | } | |
| 54 | defer resp.Body.Close() | |
| 55 | rep.StatusCode = resp.StatusCode | |
| 56 | ||
| 57 | body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) | |
| 58 | if resp.StatusCode < 200 || resp.StatusCode > 299 { | |
| 59 | rep.Detail = fmt.Sprintf("unexpected status %s from /healthz (is this really an ori server?)", resp.Status) | |
| 60 | return rep | |
| 61 | } | |
| 62 | rep.OK = true | |
| 63 | rep.Detail = describe(body) | |
| 64 | return rep | |
| 65 | } | |
| 66 | ||
| 67 | // describe turns ori's `{"status":"ok"}` body into a one-liner, tolerating | |
| 68 | // any other 2xx body so a future ori version cannot break the client. | |
| 69 | func describe(body []byte) string { | |
| 70 | var payload struct { | |
| 71 | Status string `json:"status"` | |
| 72 | } | |
| 73 | if err := json.Unmarshal(body, &payload); err == nil && payload.Status != "" { | |
| 74 | return "ori is reachable (status: " + payload.Status + ")" | |
| 75 | } | |
| 76 | return "ori is reachable" | |
| 77 | } |