Compare commits

...
21 Commits
Author SHA1 Message Date
aykhans a58ef60316 Merge pull request #232 from aykhans/feat/script-http
feat: add http function to script engines
2026-10-04 22:12:38 +04:00
aykhans 5b11ef2933 Merge remote-tracking branch 'origin/main' into feat/script-http
# Conflicts:
#	nix/package.nix
2026-10-04 22:10:02 +04:00
aykhans 3e01ce47cc nix: update vendorHash 2026-10-04 22:04:01 +04:00
aykhans 95f55e1056 nix: bump package version to 1.5.0 2026-10-04 21:59:36 +04:00
aykhans 824f03452e chore(deps): update dependencies 2026-10-04 21:58:53 +04:00
aykhans b7c589de53 fix(client): prefer IPv4 and fall back to IPv6 instead of dialing both 2026-10-04 21:50:18 +04:00
aykhans 23b3d7b1cc fix: dial dual-stack only for IPv6-only hosts 2026-10-04 15:51:50 +04:00
aykhans ab3b458f15 docs: fix statements that did not match the code 2026-10-02 02:05:49 +04:00
aykhans 51e8f2a57f fix(script): match the main request default for the script http timeout 2026-10-02 01:13:25 +04:00
aykhans a1e4c15285 docs: say what the dry run duration measures 2026-10-02 00:12:50 +04:00
aykhans bea48eeab0 docs: say that a dry run still sends a script's own requests 2026-10-01 22:43:24 +04:00
aykhans cd0fa453c2 fix(script): bound how deep json.encode walks a Lua table 2026-10-01 22:13:29 +04:00
aykhans 847c4b39ce fix(script): decode a compressed http() response body 2026-10-01 21:17:43 +04:00
aykhans 9e773a4e6c fix(script): reject a non-object headers, params or cookies in JavaScript 2026-09-30 21:18:50 +04:00
aykhans bcf2ea3446 fix(script): reject async/generator JS transforms and catch throwing getters 2026-09-30 19:53:37 +04:00
aykhans 3d535d89e3 Merge pull request #234 from aykhans/dependabot/go_modules/charm.land/bubbletea/v2-2.0.10
build(deps): bump charm.land/bubbletea/v2 from 2.0.9 to 2.0.10
2026-09-28 11:09:28 +04:00
github-actions[bot] 3a23b1b370 fix(nix): update vendorHash [dependabot skip] 2026-09-28 00:17:06 +00:00
dependabot[bot] a37885a308 build(deps): bump charm.land/bubbletea/v2 from 2.0.9 to 2.0.10
---
updated-dependencies:
- dependency-name: charm.land/bubbletea/v2
  dependency-version: 2.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-28 00:16:18 +00:00
aykhans 9de1d5ac93 Merge pull request #233 from aykhans/dependabot/go_modules/github.com/brianvoe/gofakeit/v7-7.17.1
build(deps): bump github.com/brianvoe/gofakeit/v7 from 7.17.0 to 7.17.1
2026-09-23 10:44:38 +04:00
github-actions[bot] 6fc17d1e10 fix(nix): update vendorHash [dependabot skip] 2026-09-23 00:13:47 +00:00
dependabot[bot] 4518895ab2 build(deps): bump github.com/brianvoe/gofakeit/v7 from 7.17.0 to 7.17.1
Bumps [github.com/brianvoe/gofakeit/v7](https://github.com/brianvoe/gofakeit) from 7.17.0 to 7.17.1.
- [Release notes](https://github.com/brianvoe/gofakeit/releases)
- [Commits](https://github.com/brianvoe/gofakeit/compare/v7.17.0...v7.17.1)

---
updated-dependencies:
- dependency-name: github.com/brianvoe/gofakeit/v7
  dependency-version: 7.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-23 00:12:55 +00:00
12 changed files with 305 additions and 91 deletions

No files matched your search

+9 -2
View File
@@ -181,6 +181,8 @@ A server can declare a `Content-Length` far larger than it sends, and the body i
The limit applies per response in flight, and a declared Content-Length is allocated up to it, so the worst case is a few times the limit times the concurrency. It covers the requests scripts make as well.
A compressed response in a script is decoded first, so the limit covers the decoded size as well as the bytes that arrived. Decoding holds both at once, so a brotli or zstd body peaks at over ten times the limit. Ratios are unbounded, and 106 bytes of brotli can hold 64MiB, so with `0` a tiny download can still exhaust memory.
## Concurrency
Number of concurrent workers. Must be between 1 and 100,000,000. Defaults to `1`.
@@ -201,7 +203,7 @@ Valid time units: `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`
Runtime log levels to emit, comma-separated. Valid levels: `info`, `error`. Defaults to `error`.
- `error`: errors that occur while generating or sending a request
- `error`: errors that occur while generating a request. A request that fails to send is counted in the results instead
- `info`: every completed response
Leave empty to disable logging entirely.
@@ -232,6 +234,10 @@ Using `none` disables output and reduces memory usage since response statistics
Generate requests without sending them. Useful for testing templates.
A script's own `http` calls are still sent, since a script needs them to build the request you are inspecting. Only the requests sarin generates are held back.
The duration in the results is how long generating each request took, templates and scripts included, so it is a different measurement from the response time a real run reports. A request with neither renders once before the run and reports `0s`.
## Insecure
Skip TLS certificate verification.
@@ -593,7 +599,7 @@ http(url, opts)
| `params` | table/object | - | Query parameters, added to any already in the URL |
| `cookies` | table/object | - | Cookies, sent as a single `Cookie` header |
| `body` | string | `""` | Request body |
| `timeout` | duration string (`"500ms"`) | `30s` | Request timeout (the `timeout` config does not apply), applied to each hop when redirects are followed |
| `timeout` | duration string (`"500ms"`) | `10s` | Request timeout (the `timeout` config does not apply), applied to each hop when redirects are followed |
| `insecure` | boolean | `false` | Skip TLS verification (the `insecure` config does not apply) |
| `maxRedirects` | number | `0` | How many redirects to follow, `0` follows none, maximum 100 |
@@ -622,6 +628,7 @@ In Lua, call the helpers as `res:header("name")` or `res.header("name")`.
- **Status codes:** non-2xx responses are returned normally; check `status` in the script.
- **Errors:** network failures and timeouts raise a script error. The main request is not sent and the error is counted in the results. Scripts can catch it with `pcall` (Lua) or `try`/`catch` (JavaScript).
- **Response bodies:** the [Max Response Body](#max-response-body) config bounds these requests too, and a body above it raises an error the script can catch.
- **Compressed responses:** sarin asks for no encoding, so this only applies when a script sends `Accept-Encoding` itself or the server compresses unasked. A `gzip`, `deflate`, `br` or `zstd` body is then decoded before the script sees it, and `Content-Encoding` and `Content-Length` are both dropped from `headers` because neither describes what the script receives. Anything else is left raw with its header intact: an empty body, `identity`, a `Content-Encoding` sarin does not recognise, and two or more codings at once. A body that fails to decode raises an error the script can catch, though `br` carries no checksum, so damage to a brotli body can go unnoticed.
- **Only inside `transform`:** `http` works anywhere while `transform` runs, including in helper functions it calls. Calling it at the top level of a script raises an error, because that code also runs during config validation and when each worker starts.
- **Dry run:** script requests are still sent in dry-run mode.
- **Not measured:** script requests are not included in the results.
+3 -4
View File
@@ -857,7 +857,7 @@ progress: none
## Runtime Logging
`--log-level` selects which runtime logs Sarin emits (comma-separated `info` and `error`, default `error`). `error` covers request and generation errors, `info` covers every completed response (status, duration, headers, body). Logs appear in the progress log box on an interactive terminal, go to stderr when piped, or go to a file with `--log-file`.
`--log-level` selects which runtime logs Sarin emits (comma-separated `info` and `error`, default `error`). `error` covers errors raised while generating a request, `info` covers every completed response (status, duration, headers, body). A request that fails to send is counted in the results rather than logged. Logs appear in the progress log box on an interactive terminal, go to stderr when piped, or go to a file with `--log-file`.
**Log responses and errors:**
@@ -883,9 +883,8 @@ sarin -U http://example.com -r 1000 -l info -o json > stats.json 2> run.log
```yaml
url: http://example.com
requests: 1000
concurrency: 10
logLevel: info,error
logFile: ./run.log
logLevel: info
output: json
```
</details>
+6 -3
View File
@@ -2,7 +2,7 @@
Sarin supports Go templates in URL paths, methods, bodies, headers, params, cookies, and values.
> **Note:** Templating in URL host and scheme is not supported. Only the path portion of the URL can contain templates.
> **Note:** Templating in URL host and scheme is not supported. Only the path and query string of the URL can contain templates.
> **Note:** Template rendering happens before the request is sent. The request timeout (`-T` / `timeout`) only governs the HTTP request itself and starts _after_ templates have finished rendering, so slow template functions (e.g. captcha solvers, remote `file_Read`) cannot cause a request timeout no matter how long they take. Downloads have their own limits: 30 seconds and 64MiB per file.
@@ -29,6 +29,7 @@ Sarin supports Go templates in URL paths, methods, bodies, headers, params, cook
- [Generate](#generate)
- [Auth](#auth)
- [Address](#address)
- [Airline](#airline)
- [Game](#game)
- [Beer](#beer)
- [Car](#car)
@@ -151,7 +152,7 @@ body: '{{ json_Encode (dict_Str "key1" "value1" "key2" "value2") }}'
| ------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------- |
| `time_NowUnix` | Current Unix timestamp (seconds) | `{{ time_NowUnix }}` → `1735689600` |
| `time_NowUnixMilli` | Current Unix timestamp (milliseconds) | `{{ time_NowUnixMilli }}` → `1735689600123` |
| `time_NowRFC3339` | Current time in RFC3339 format | `{{ time_NowRFC3339 }}` → `"2026-02-26T21:00:00Z"` |
| `time_NowRFC3339` | Current time in RFC3339 format | `{{ time_NowRFC3339 }}` → `"2026-02-26T21:00:00+04:00"` (local time) |
| `time_Format(layout, t)` | Format a `time.Time` value with a Go layout | `{{ time_Format "2006-01-02" (strings_ToDate "2024-05-10") }}` → `"2024-05-10"` |
### Crypto Functions
@@ -165,6 +166,8 @@ body: '{{ json_Encode (dict_Str "key1" "value1" "key2" "value2") }}'
### Body Functions
> **Note:** These are available in `body` only, not in the URL, headers, params, cookies or values.
| Function | Description | Example |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `body_FormData(pairs ...string)` | Create multipart form data from key-value pairs. Automatically sets the `Content-Type` header. Values starting with `@` are treated as file references (local path or URL). Use `@@` to escape literal `@`. | `{{ body_FormData "field1" "value1" "file" "@/path/to/file.pdf" }}` |
@@ -387,7 +390,7 @@ These functions are powered by [gofakeit](https://github.com/brianvoe/gofakeit)
| Function | Description | Example |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------- |
| `fakeit_Username` | Username | `"Daniel1364"` |
| `fakeit_Password(upper bool, lower bool, numeric bool, special bool, space bool, length int)` | Generate password with specified character types and length | `{{ fakeit_Password true true true false false 16 }}` |
| `fakeit_Password(lower bool, upper bool, numeric bool, special bool, space bool, length int)` | Generate password with specified character types and length | `{{ fakeit_Password true true true false false 16 }}` |
### Address
+10 -10
View File
@@ -4,12 +4,12 @@ go 1.27.1
require (
charm.land/bubbles/v2 v2.2.1
charm.land/bubbletea/v2 v2.0.9
charm.land/bubbletea/v2 v2.0.10
charm.land/glamour/v2 v2.0.1
charm.land/lipgloss/v2 v2.0.6
github.com/brianvoe/gofakeit/v7 v7.17.0
github.com/brianvoe/gofakeit/v7 v7.17.1
github.com/charmbracelet/x/term v0.2.2
github.com/dop251/goja v0.0.0-20260911104922-fabc3b8078ad
github.com/dop251/goja v0.0.0-20261004174310-e21e22eac913
github.com/joho/godotenv v1.5.1
github.com/valyala/fasthttp v1.74.0
github.com/yuin/gopher-lua v1.1.2
@@ -23,28 +23,28 @@ require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20261001125412-878653296cfd // indirect
github.com/charmbracelet/x/ansi v0.11.8 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20260906004030-3986e9119cf9 // indirect
github.com/charmbracelet/x/exp/slice v0.1.0 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2/v2 v2.8.0 // indirect
github.com/dlclark/regexp2/v2 v2.8.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe // indirect
github.com/google/pprof v0.0.0-20261003200830-ebaad5f31b4d // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
github.com/mattn/go-runewidth v0.0.30 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/molecule-man/go-brrr v1.1.0 // indirect
github.com/molecule-man/go-brrr v1.2.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xo/terminfo v1.0.0 // indirect
github.com/xo/terminfo v1.2.0 // indirect
github.com/yuin/goldmark v1.8.6 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
golang.org/x/sync v0.23.0 // indirect
+20 -22
View File
@@ -1,7 +1,7 @@
charm.land/bubbles/v2 v2.2.1 h1:Fq1+qm5hV6GkvzLQDhCBpXXE5tLgvh1PRriCLwSvIQU=
charm.land/bubbles/v2 v2.2.1/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc=
charm.land/bubbletea/v2 v2.0.9 h1:DpJCMWKgzQK8SJv4zbKKFHAI10ymWy/evClPFk0k0f8=
charm.land/bubbletea/v2 v2.0.9/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss=
charm.land/bubbletea/v2 v2.0.10 h1:oolvo20VBpI0PfqE7iFjkZ1bx0WpmXGfnKz5Yldjq5o=
charm.land/bubbletea/v2 v2.0.10/go.mod h1:QOatcnhOjYIfxzUSTz6raF7Ex4R/rIuHa3SnBdCCpMc=
charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c=
charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k=
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
@@ -18,20 +18,20 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/brianvoe/gofakeit/v7 v7.17.0 h1:usvHa73vmAjC1JmX4dpZAm1eQkR0riXaxvilScRTFb0=
github.com/brianvoe/gofakeit/v7 v7.17.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/brianvoe/gofakeit/v7 v7.17.1 h1:50FLBhTGVJQaj6ysRUu0it8wCdYO2uGM9VfuxI+csEc=
github.com/brianvoe/gofakeit/v7 v7.17.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16 h1:/hSCZO1puetbGd2JikHKN0nAcb2mFevzxJElFoduhaw=
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16/go.mod h1:JFvmQPZh3uAu7PpSZU91bcR0BrIy24HhW+0EsW1uGxE=
github.com/charmbracelet/ultraviolet v0.0.0-20261001125412-878653296cfd h1:W0HOPGkwRaAmV20plE/f+Rk6Xn+IdR8G0qHsGXcOnF8=
github.com/charmbracelet/ultraviolet v0.0.0-20261001125412-878653296cfd/go.mod h1:D9SVXVpAsD1pP6xR6BiERbe4+dUU6EW9US1TFrNutGA=
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/slice v0.0.0-20260906004030-3986e9119cf9 h1:zW96vcZ3zDetccerbN7sS/sCydOE/l/9S+C94pozsaU=
github.com/charmbracelet/x/exp/slice v0.0.0-20260906004030-3986e9119cf9/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
github.com/charmbracelet/x/exp/slice v0.1.0 h1:m46dwuDf3M5SFYMRhxYkLC6z9WnENGVKbcbJlp13/og=
github.com/charmbracelet/x/exp/slice v0.1.0/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
@@ -44,32 +44,32 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw=
github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dop251/goja v0.0.0-20260911104922-fabc3b8078ad h1:5gT9hQ9h8CplLZfAGgFMcoLqQS6I9T2sZwQOCXy8/3s=
github.com/dop251/goja v0.0.0-20260911104922-fabc3b8078ad/go.mod h1:u8yZRUavu+N4EnFFy6J5fVtjE7lEcZ2YyV2GcBXY9c8=
github.com/dlclark/regexp2/v2 v2.8.2 h1:uTTe+cxADL4be+dv4DZwsBIEPCQ6RHPlnVuKrtLqSx0=
github.com/dlclark/regexp2/v2 v2.8.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dop251/goja v0.0.0-20261004174310-e21e22eac913 h1:0haZHQSJqA4qFZvnKIRgekXrPo2sQ2oRvFemiNHhcp0=
github.com/dop251/goja v0.0.0-20261004174310-e21e22eac913/go.mod h1:GrCNKUrVplXZj0juFue0Cj2Xt/eL2Nuu2+y3UTanEo8=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe h1:QAinXoAFJdGQYztXn3VpFey7KCwpedbZ/EkzbplQ0cY=
github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/pprof v0.0.0-20261003200830-ebaad5f31b4d h1:kZK3FQM86gr3cgJHMP7rPn8A3Z75mt+ZaVBoqMlpPF0=
github.com/google/pprof v0.0.0-20261003200830-ebaad5f31b4d/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/molecule-man/go-brrr v1.1.0 h1:rLBGSXA393y+5ttCBLkW34CHESop5tahrOFxVv/XDEU=
github.com/molecule-man/go-brrr v1.1.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
github.com/molecule-man/go-brrr v1.2.0 h1:dOJU45BC3Gc2WXoxAW0rgCAgVuu2gruAOGncfieA0Jw=
github.com/molecule-man/go-brrr v1.2.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@@ -82,8 +82,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.74.0 h1:wMS9fnO2QTALozYx5pId2Vi7ZwU/epUkY8i/KPWCHoU=
github.com/valyala/fasthttp v1.74.0/go.mod h1:3ARmLamUcw7ElxVtC8PXaGzQ6VEuvnetlkrwIklQBSE=
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4=
github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
@@ -94,8 +94,6 @@ go.aykhans.me/utils v1.0.7 h1:ClHXHlWmkjfFlD7+w5BQY29lKCEztxY/yCf543x4hZw=
go.aykhans.me/utils v1.0.7/go.mod h1:0Jz8GlZLN35cCHLOLx39sazWwEe33bF6SYlSeqzEXoI=
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
+3 -3
View File
@@ -31,7 +31,7 @@ Flags:
-w, -log-file string Write runtime logs to this file instead of the terminal/stderr
-p, -progress string Progress display (possible values: bar, none) (default '%v')
-o, -output string Output format (possible values: table, json, yaml, none) (default '%v')
-z, -dry-run bool Run without sending requests (default %v)
-z, -dry-run bool Run without sending the generated requests (default %v)
Request Config:
-U, -url string Target URL for the request
@@ -145,8 +145,8 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
flagSet.StringVar(&output, "output", "", "Output format (possible values: table, json, yaml, none)")
flagSet.StringVar(&output, "o", "", "Output format (possible values: table, json, yaml, none)")
flagSet.BoolVar(&dryRun, "dry-run", false, "Run without sending requests")
flagSet.BoolVar(&dryRun, "z", false, "Run without sending requests")
flagSet.BoolVar(&dryRun, "dry-run", false, "Run without sending the generated requests")
flagSet.BoolVar(&dryRun, "z", false, "Run without sending the generated requests")
// Request config
flagSet.StringVar(&urlInput, "url", "", "Target URL for the request")
+88 -4
View File
@@ -19,6 +19,90 @@ import (
// clientIndexGenerator returns the index of the host client (and proxy) to use next.
type clientIndexGenerator func() int
// preferIPv4Resolver hides a host's IPv6 addresses whenever it also has IPv4 ones.
type preferIPv4Resolver struct{}
func (preferIPv4Resolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) {
return lookupPreferred(ctx, host, true)
}
// preferIPv6Resolver is the mirror of preferIPv4Resolver, used for the fallback dial.
type preferIPv6Resolver struct{}
func (preferIPv6Resolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) {
return lookupPreferred(ctx, host, false)
}
// lookupPreferred resolves host and keeps only the wanted family, unless that family is absent.
func lookupPreferred(ctx context.Context, host string, wantIPv4 bool) ([]net.IPAddr, error) {
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
preferred := make([]net.IPAddr, 0, len(ips))
for _, ip := range ips {
if (ip.IP.To4() != nil) == wantIPv4 {
preferred = append(preferred, ip)
}
}
if len(preferred) > 0 {
return preferred, nil
}
return ips, nil
}
// preferIPv4DialTimeoutShare leaves half the timeout for the IPv6 retry.
const preferIPv4DialTimeoutShare = 2
// preferIPv4Dialer dials over IPv4 and retries over IPv6 alone when that fails.
type preferIPv4Dialer struct {
ipv4 *fasthttp.TCPDialer
ipv6 *fasthttp.TCPDialer
}
// dialerConcurrency matches the limit on fasthttp own package-level dialer.
const dialerConcurrency = 1000
// dialer is shared so all clients use one DNS cache.
var dialer = &preferIPv4Dialer{
ipv4: &fasthttp.TCPDialer{Resolver: preferIPv4Resolver{}, Concurrency: dialerConcurrency},
ipv6: &fasthttp.TCPDialer{Resolver: preferIPv6Resolver{}, Concurrency: dialerConcurrency},
}
// DialTimeout dials addr, which must carry a port.
func (d *preferIPv4Dialer) DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {
// fasthttp passes 0 from its pooled dial path.
if timeout <= 0 {
timeout = fasthttp.DefaultDialTimeout
}
deadline := time.Now().Add(timeout)
conn, err := d.ipv4.DialDualStackTimeout(addr, timeout/preferIPv4DialTimeoutShare)
if err == nil {
return conn, nil
}
remaining := time.Until(deadline)
if remaining <= 0 {
return nil, err
}
conn, fallbackErr := d.ipv6.DialDualStackTimeout(addr, remaining)
if fallbackErr != nil {
// Report the IPv4 error, not the fallback one.
return nil, err
}
return conn, nil
}
// dialFunc fills in the default port, which fasthttp skips for a custom dial func.
func (d *preferIPv4Dialer) dialFunc(isTLS bool) fasthttp.DialFuncWithTimeout {
return func(addr string, timeout time.Duration) (net.Conn, error) {
return d.DialTimeout(fasthttp.AddMissingPort(addr, isTLS), timeout)
}
}
// NewHostClients creates a list of fasthttp.HostClient instances for the given proxies.
// If no proxies are provided, a single client without a proxy is returned.
// It can return the following errors:
@@ -73,9 +157,9 @@ func NewHostClients(
}
client := &fasthttp.HostClient{
MaxConns: safeUintToInt(maxConns),
IsTLS: isTLS,
DialDualStack: true,
MaxConns: safeUintToInt(maxConns),
IsTLS: isTLS,
DialTimeout: dialer.dialFunc(isTLS),
TLSConfig: &tls.Config{
InsecureSkipVerify: skipVerify, //nolint:gosec
},
@@ -256,7 +340,7 @@ func fasthttpConnectDialer(proxyURL *url.URL, useTLS bool) fasthttp.DialFuncWith
return func(addr string, timeout time.Duration) (net.Conn, error) {
// Establish TCP connection to proxy with timeout
start := time.Now()
conn, err := fasthttp.DialDualStackTimeout(proxyAddr, timeout)
conn, err := dialer.DialTimeout(proxyAddr, timeout)
if err != nil {
return nil, types.NewProxyDialError(proxyStr, err)
}
+46 -12
View File
@@ -1,6 +1,7 @@
package sarin
import (
"compress/zlib"
"context"
"crypto/tls"
"errors"
@@ -13,15 +14,16 @@ import (
"go.aykhans.me/sarin/internal/types"
)
// scriptHTTPDefaultTimeout is the default timeout for script requests, independent of -T.
const scriptHTTPDefaultTimeout = 30 * time.Second
// scriptHTTPDefaultTimeout is the default timeout for script requests.
const scriptHTTPDefaultTimeout = 10 * time.Second
// scriptHTTPClient sends scripts' http.* requests. It is safe for concurrent use.
type scriptHTTPClient struct {
ctx context.Context //nolint:containedctx
client *fasthttp.Client
insecureClient *fasthttp.Client
defaultTimeout time.Duration
ctx context.Context //nolint:containedctx
client *fasthttp.Client
insecureClient *fasthttp.Client
defaultTimeout time.Duration
maxResponseBody int
}
var _ script.HTTPDoer = (*scriptHTTPClient)(nil)
@@ -31,7 +33,7 @@ var _ script.HTTPDoer = (*scriptHTTPClient)(nil)
// - types.ProxyDialError
func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint, maxResponseBody uint64) ([]*scriptHTTPClient, error) {
if len(proxies) == 0 {
return []*scriptHTTPClient{newScriptHTTPClient(ctx, fasthttp.DialDualStackTimeout, scriptHTTPDefaultTimeout, maxConns, maxResponseBody)}, nil
return []*scriptHTTPClient{newScriptHTTPClient(ctx, dialer.DialTimeout, scriptHTTPDefaultTimeout, maxConns, maxResponseBody)}, nil
}
clients := make([]*scriptHTTPClient, 0, len(proxies))
@@ -71,10 +73,11 @@ func newScriptHTTPClient(
}
return &scriptHTTPClient{
ctx: ctx,
client: newClient(false),
insecureClient: newClient(true),
defaultTimeout: defaultTimeout,
ctx: ctx,
client: newClient(false),
insecureClient: newClient(true),
defaultTimeout: defaultTimeout,
maxResponseBody: safeUint64ToInt(maxResponseBody),
}
}
@@ -166,7 +169,10 @@ func (c *scriptHTTPClient) Do(r *script.HTTPRequest) (*script.HTTPResponse, erro
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, err)
}
body := resp.Body()
body, err := decodeResponseBody(resp, c.maxResponseBody)
if err != nil {
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, err)
}
return &script.HTTPResponse{
Status: resp.StatusCode(),
@@ -174,3 +180,31 @@ func (c *scriptHTTPClient) Do(r *script.HTTPRequest) (*script.HTTPResponse, erro
Body: string(body),
}, nil
}
// decodeResponseBody decodes a body the server compressed, since scripts read it as text.
// The limit covers the decoded size, which is what a compression bomb inflates to.
// An empty body is left alone because the decoders reject zero bytes, and a HEAD or a 304
// carries the encoding with no body. A coding fasthttp does not decode, and the bare
// deflate stream some servers send without a zlib header, are left raw for the script.
func decodeResponseBody(resp *fasthttp.Response, maxResponseBody int) ([]byte, error) {
if len(resp.Header.ContentEncoding()) == 0 || len(resp.Body()) == 0 {
return resp.Body(), nil
}
decoded, err := resp.BodyUncompressedWithLimit(maxResponseBody)
if errors.Is(err, fasthttp.ErrContentEncodingUnsupported) || errors.Is(err, zlib.ErrHeader) {
return resp.Body(), nil
}
if err != nil {
if errors.Is(err, fasthttp.ErrBodyTooLarge) {
err = types.ErrResponseBodyTooLarge
}
return nil, err
}
// Neither header describes the decoded body, so both go, as net/http does on a gunzip.
resp.Header.SetContentEncoding("")
resp.Header.Del(fasthttp.HeaderContentLength)
return decoded, nil
}
+105 -29
View File
@@ -2,6 +2,8 @@ package script
import (
"errors"
"fmt"
"reflect"
"strconv"
"github.com/dop251/goja"
@@ -18,6 +20,9 @@ type JsEngine struct {
http httpBridge
}
// promiseType is what a transform returns when it is declared async or as a generator.
var promiseType = reflect.TypeFor[*goja.Promise]()
// NewJsEngine creates a new JavaScript script engine with the given script content.
// The script must define a global `transform` function that takes a request object
// and returns the modified request object.
@@ -42,8 +47,7 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
}
// Execute the script to define the transform function
_, err := vm.RunString(scriptContent)
if err != nil {
if _, err := vm.RunString(scriptContent); err != nil {
return nil, types.NewScriptExecutionError("JavaScript", engine.exceptionError(err))
}
@@ -58,6 +62,12 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
return nil, types.NewScriptExecutionError("JavaScript", errors.New("'transform' must be a function"))
}
// A generator returns an iterator, which is an ordinary object to Go, so the
// call cannot tell it apart and the declaration is the only thing left to read.
if isGeneratorFunction(vm, transformVal) {
return nil, types.ErrScriptTransformAsync
}
engine.transform = transform
return engine, nil
@@ -84,6 +94,13 @@ func (e *JsEngine) Transform(req *RequestData) error {
return types.NewScriptExecutionError("JavaScript", e.exceptionError(err))
}
// An async or generator transform returns a promise nothing ever awaits, and a promise
// carries none of the request fields, so the request would go out untransformed.
// ExportType reads the type alone, while Export would run every getter on the result.
if result.ExportType() == promiseType {
return types.ErrScriptTransformAsync
}
// Update RequestData from the returned object
if err := e.objectToRequestData(result, req); err != nil {
return types.NewScriptExecutionError("JavaScript", err)
@@ -144,37 +161,70 @@ func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
return types.ErrScriptTransformReturnObject
}
// Method
if v := obj.Get("method"); v != nil && !goja.IsUndefined(v) {
req.Method = v.String()
// Reading a field runs the script's own getters and toString, which throw as a Go
// panic outside goja's execution context, so the whole read runs inside Try.
var fieldErr error
if thrown := e.runtime.Try(func() {
// Method
if v := obj.Get("method"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Method = v.String()
}
// Path
if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Path = v.String()
}
// Body
if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Body = v.String()
}
// Headers
if v := obj.Get("headers"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
values, err := objectField("headers", v)
if err != nil {
fieldErr = err
return
}
req.Headers = e.objectToStringSliceMap(values)
}
// Params
if v := obj.Get("params"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
values, err := objectField("params", v)
if err != nil {
fieldErr = err
return
}
req.Params = e.objectToStringSliceMap(values)
}
// Cookies
if v := obj.Get("cookies"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
values, err := objectField("cookies", v)
if err != nil {
fieldErr = err
return
}
req.Cookies = e.objectToStringSliceMap(values)
}
}); thrown != nil {
return e.exceptionError(thrown)
}
// Path
if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) {
req.Path = v.String()
}
return fieldErr
}
// Body
if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) {
req.Body = v.String()
// objectField rejects anything that is not a plain object, since reading keys off an
// array or a boxed string yields headers named after indexes and off a Date yields none.
// A Map, a Set and a typed array still pass, so the field is emptied rather than refused.
func objectField(name string, value goja.Value) (*goja.Object, error) {
obj, ok := value.(*goja.Object)
if !ok || obj.ClassName() != "Object" {
return nil, fmt.Errorf("%s must be an object", name)
}
// Headers
if v := obj.Get("headers"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Headers = e.objectToStringSliceMap(v.ToObject(e.runtime))
}
// Params
if v := obj.Get("params"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Params = e.objectToStringSliceMap(v.ToObject(e.runtime))
}
// Cookies
if v := obj.Get("cookies"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Cookies = e.objectToStringSliceMap(v.ToObject(e.runtime))
}
return nil
return obj, nil
}
// stringSliceToArray converts a Go []string to a JavaScript array.
@@ -245,3 +295,29 @@ func jsStringValue(v goja.Value) (string, bool) {
}
return v.String(), true
}
// isGeneratorFunction reports whether a function is declared with a star.
// The name comes from the script's own prototype chain, so a script can falsify it either way.
// Reading it can run a script getter, so the whole walk stays inside Try.
func isGeneratorFunction(runtime *goja.Runtime, value goja.Value) bool {
var name string
thrown := runtime.Try(func() {
obj := value.ToObject(runtime)
if obj == nil {
return
}
constructor := obj.Get("constructor")
if constructor == nil || goja.IsUndefined(constructor) || goja.IsNull(constructor) {
return
}
constructorObj := constructor.ToObject(runtime)
if constructorObj == nil {
return
}
if value := constructorObj.Get("name"); value != nil {
name = value.String()
}
})
return thrown == nil && name == "GeneratorFunction"
}
+11
View File
@@ -45,9 +45,14 @@ func luaJSONDecode(state *lua.LState) int {
return 1
}
// luaJSONMaxDepth matches the depth encoding/json accepts, so a table a script
// decoded always encodes again, and it stays far below what overflows the Go stack.
const luaJSONMaxDepth = 10000
// luaToGo converts a Lua value to a JSON-encodable Go value.
// It can return the following errors:
// - types.ErrScriptJSONCycle
// - types.ErrScriptJSONDepth
// - types.ScriptTypeError
func luaToGo(value lua.LValue, visiting map[*lua.LTable]bool) (any, error) {
switch v := value.(type) {
@@ -63,6 +68,12 @@ func luaToGo(value lua.LValue, visiting map[*lua.LTable]bool) (any, error) {
if visiting[v] {
return nil, types.ErrScriptJSONCycle
}
// visiting holds the tables on the current path, so its size is the depth.
// A table millions of levels deep overflows the Go stack, which is a fatal
// error no pcall can catch, so the descent is bounded well before that.
if len(visiting) >= luaJSONMaxDepth {
return nil, types.ErrScriptJSONDepth
}
visiting[v] = true
defer delete(visiting, v)
+2
View File
@@ -434,6 +434,7 @@ var (
ErrScriptSourceEmpty = errors.New("script source cannot be empty after @")
ErrScriptTransformMissing = errors.New("script must define a global 'transform' function")
ErrScriptTransformReturnObject = errors.New("transform function must return an object")
ErrScriptTransformAsync = errors.New("transform function must be synchronous, async and generator functions are not supported")
ErrScriptURLNoHost = errors.New("script URL must have a host")
ErrScriptHTTPOutsideTransform = errors.New("http functions can only be called while transform is running")
ErrScriptHTTPUnavailable = errors.New("http client is not available")
@@ -441,6 +442,7 @@ var (
ErrScriptHTTPURLInvalid = errors.New("URL must be an absolute http or https URL")
ErrScriptHTTPUnknownOption = errors.New("unknown option")
ErrScriptJSONCycle = errors.New("cannot encode a table that contains itself")
ErrScriptJSONDepth = errors.New("cannot encode a table nested too deeply")
)
type ScriptTypeError struct {
+2 -2
View File
@@ -8,11 +8,11 @@
(buildGoModule.override { go = go_1_27; }) (finalAttrs: {
pname = "sarin";
version = "1.4.4"; # bump per release
version = "1.5.0"; # bump per release
src = lib.cleanSource ../.;
vendorHash = "sha256-KUxzFDxYbKvKASMze+f7jxfpCQbs01ezeG6a1VboHxw=";
vendorHash = "sha256-SpMOMmDa7llS8wbCDf6QJbWtZ/s8RgzMvVP1xrY934s=";
subPackages = [ "cmd/cli" ];