{"id":24638215,"url":"https://github.com/miyamo2/r2","last_synced_at":"2025-05-08T23:44:43.578Z","repository":{"id":252694227,"uuid":"836528812","full_name":"miyamo2/r2","owner":"miyamo2","description":"Proof of concept for the Go 1.22 range functions and provides a simple and easy-to-use interface for sending HTTP requests with retries.","archived":false,"fork":false,"pushed_at":"2024-09-08T02:14:07.000Z","size":154,"stargazers_count":6,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-08T23:44:36.089Z","etag":null,"topics":["go","golang","polling","rangefunc","retry"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/miyamo2.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-01T03:34:43.000Z","updated_at":"2024-10-06T13:55:30.000Z","dependencies_parsed_at":"2024-08-23T16:12:35.697Z","dependency_job_id":null,"html_url":"https://github.com/miyamo2/r2","commit_stats":null,"previous_names":["miyamo2/r2"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miyamo2%2Fr2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miyamo2%2Fr2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miyamo2%2Fr2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miyamo2%2Fr2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/miyamo2","download_url":"https://codeload.github.com/miyamo2/r2/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253166473,"owners_count":21864467,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","polling","rangefunc","retry"],"created_at":"2025-01-25T10:13:28.587Z","updated_at":"2025-05-08T23:44:43.559Z","avatar_url":"https://github.com/miyamo2.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# r2 - __range__ over http __request__\n[![Go Reference](https://pkg.go.dev/badge/github.com/miyamo2/r2.svg)](https://pkg.go.dev/github.com/miyamo2/r2)\n[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/miyamo2/r2)](https://img.shields.io/github/go-mod/go-version/miyamo2/r2)\n[![GitHub release (latest by date)](https://img.shields.io/github/v/release/miyamo2/r2)](https://img.shields.io/github/v/release/miyamo2/r2)\n[![codecov](https://codecov.io/gh/miyamo2/r2/graph/badge.svg?token=NL0BQIIAZJ)](https://codecov.io/gh/miyamo2/r2)\n[![Go Report Card](https://goreportcard.com/badge/github.com/miyamo2/r2)](https://goreportcard.com/report/github.com/miyamo2/r2)\n[![GitHub License](https://img.shields.io/github/license/miyamo2/r2?\u0026color=blue)](https://img.shields.io/github/license/miyamo2/r2?\u0026color=blue)\n\n**r2** is a proof of concept for the Go 1.22 range functions and provides a simple and easy-to-use interface for sending HTTP requests with retries.\n\n## Quick Start\n\n### Install\n\n```sh\ngo get github.com/miyamo2/r2\n```\n\n### Setup `GOEXPERIMENT`\n\n\u003e [!IMPORTANT]\n\u003e\n\u003e If your Go project is Go 1.23 or higher, this section is not necessary.\n\n```sh\ngo env -w GOEXPERIMENT=rangefunc\n```\n\n### Simple Usage\n\n```go\nurl := \"http://example.com\"\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n}\nfor res, err := range r2.Get(ctx, url, opts...) {\n\tif err != nil {\n\t\tslog.WarnContext(ctx, \"something happened.\", slog.Any(\"error\", err))\n\t\t// Note: Even if continue is used, the iterator could be terminated.\n\t\t// Likewise, if break is used, the request could be re-executed in the background once more.\n\t\tcontinue\n\t}\n\tif res == nil {\n\t\tslog.WarnContext(ctx, \"response is nil\")\n\t\tcontinue\n\t}\n\tif res.StatusCode != http.StatusOK {\n\t\tslog.WarnContext(ctx, \"unexpected status code.\", slog.Int(\"expect\", http.StatusOK), slog.Int(\"got\", res.StatusCode))\n\t\tcontinue\n\t}\n\n\tbuf, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\tslog.ErrorContext(ctx, \"failed to read response body.\", slog.Any(\"error\", err))\n\t\tcontinue\n\t}\n\tslog.InfoContext(ctx, \"response\", slog.String(\"response\", string(buf)))\n\t// There is no need to close the response body yourself as auto closing is enabled by default.\n}\n```\n\n\u003cdetails\u003e\n    \u003csummary\u003evs 'github.com/avast/retry-go'\u003c/summary\u003e\n\n```go\nurl := \"http://example.com\"\nvar buf []byte\n\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\n\ntype ErrTooManyRequests struct{\n\terror\n\tRetryAfter time.Duration\n}\n\nopts := []retry.Option{\n\tretry.Attempts(3),\n\tretry.Context(ctx),\n\t// In r2, the delay is calculated with the backoff and jitter by default. \n\t// And, if 429 Too Many Requests are returned, the delay is set based on the Retry-After.\n\tretry.DelayType(\n\t\tfunc(n uint, err error, config *Config) time.Duration {\n\t\t\tif err != nil {\n\t\t\t\tvar errTooManyRequests ErrTooManyRequests\n\t\t\t\tif errors.As(err, \u0026ErrTooManyRequests) {\n\t\t\t\t\tif ErrTooManyRequests.RetryAfter != 0 {\n\t\t\t\t\t\treturn ErrTooManyRequests.RetryAfter\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn retry.BackOffDelay(n, err, config)\n\t\t}),\n}\n\n// In r2, the timeout period per request can be specified with the `WithPeriod` option.\nclient := http.Client{\n\tTimeout: time.Second,\n}\n\nerr := retry.Do(\n\tfunc() error {\n\t\tres, err := client.Get(url)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif res == nil {\n\t\t\treturn fmt.Errorf(\"response is nil\")\n\t\t}\n\t\tif res.StatusCode == http.StatusTooManyRequests {\n\t\t\tretryAfter := res.Header.Get(\"Retry-After\")\n\t\t\tif retryAfter != \"\" {\n\t\t\t\tretryAfterDuration, err := time.ParseDuration(retryAfter)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \u0026ErrTooManyRequests{error: fmt.Errorf(\"429: too many requests\")}\n\t\t\t\t}\n\t\t\t\treturn \u0026ErrTooManyRequests{error: fmt.Errorf(\"429: too many requests\"), RetryAfter: retryAfterDuration}\n\t\t\t}\n\t\t\treturn \u0026ErrTooManyRequests{error: fmt.Errorf(\"429: too many requests\")}\n\t\t}\n\t\tif res.StatusCode \u003e= http.StatusBadRequest \u0026\u0026 res.StatusCode \u003c http.StatusInternalServerError {\n\t\t\t// In r2, client errors other than TooManyRequests are excluded from retries by default.\n\t\t\treturn nil\n\t\t}\n\t\tif res.StatusCode \u003e= http.StatusInternalServerError {\n\t\t\t// In r2, automatically retry if the server error response is returned by default.\n\t\t\treturn fmt.Errorf(\"5xx: server error response\")\n\t\t}\n\n\t\tif res.StatusCode != http.StatusOK {\n\t\t\treturn fmt.Errorf(\"unexpected status code: expected %d, got %d\", http.StatusOK, res.StatusCode)\n\t\t}\n\n\t\t// In r2, the response body is automatically closed by default.\n\t\tdefer res.Body.Close()\n\t\tbuf, err = io.ReadAll(res.Body)\n\t\tif err != nil {\n\t\t\tslog.ErrorContext(ctx, \"failed to read response body.\", slog.Any(\"error\", err))\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t},\n\topts...,\n)\n\nif err != nil {\n\t// handle error\n}\n\nslog.InfoContext(ctx, \"response\", slog.String(\"response\", string(buf)))\n```\n\u003c/details\u003e\n\n### Features\n\n| Feature                                                                 | Description                                                                                                                                                       |\n|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| [`Get`](https://github.com/miyamo2/r2?tab=readme-ov-file#get)           | Send HTTP Get requests until the [termination condition](https://github.com/miyamo2/r2?tab=readme-ov-file#termination-conditions) is satisfied.                   |\n| [`Head`](https://github.com/miyamo2/r2?tab=readme-ov-file#head)         | Send HTTP Head requests until the [termination condition](https://github.com/miyamo2/r2?tab=readme-ov-file#termination-conditions) is satisfied.                  |\n| [`Post`](https://github.com/miyamo2/r2?tab=readme-ov-file#post)         | Send HTTP Post requests until the [termination condition](https://github.com/miyamo2/r2?tab=readme-ov-file#termination-conditions) is satisfied.                  |\n| [`Put`](https://github.com/miyamo2/r2?tab=readme-ov-file#put)           | Send HTTP Put requests until the [termination condition](https://github.com/miyamo2/r2?tab=readme-ov-file#termination-conditions) is satisfied.                   |\n| [`Patch`](https://github.com/miyamo2/r2?tab=readme-ov-file#patch)       | Send HTTP Patch requests until the [termination condition](https://github.com/miyamo2/r2?tab=readme-ov-file#termination-conditions) is satisfied.                 |\n| [`Delete`](https://github.com/miyamo2/r2?tab=readme-ov-file#delete)     | Send HTTP Delete requests until the [termination condition](https://github.com/miyamo2/r2?tab=readme-ov-file#termination-conditions) is satisfied.                |\n| [`PostForm`](https://github.com/miyamo2/r2?tab=readme-ov-file#postform) | Send HTTP Post requests with form until the [termination condition](https://github.com/miyamo2/r2?tab=readme-ov-file#termination-conditions) is satisfied.        |\n| [`Do`](https://github.com/miyamo2/r2?tab=readme-ov-file#do)             | Send HTTP requests with the given method until the [termination condition](https://github.com/miyamo2/r2?tab=readme-ov-file#termination-conditions) is satisfied. |\n\n#### Get\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### Head\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n}\nfor res, err := range r2.Head(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### Post\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n\tr2.WithContentType(r2.ContentTypeApplicationJson),\n}\nbody := bytes.NewBuffer([]byte(`{\"foo\": \"bar\"}`))\nfor res, err := range r2.Post(ctx, \"https://example.com\", body, opts...) {\n\t// do something\n}\n```\n\n#### Put\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n\tr2.WithContentType(r2.ContentTypeApplicationJson),\n}\nbody := bytes.NewBuffer([]byte(`{\"foo\": \"bar\"}`))\nfor res, err := range r2.Put(ctx, \"https://example.com\", body, opts...) {\n\t// do something\n}\n```\n\n#### Patch\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n\tr2.WithContentType(r2.ContentTypeApplicationJson),\n}\nbody := bytes.NewBuffer([]byte(`{\"foo\": \"bar\"}`))\nfor res, err := range r2.Patch(ctx, \"https://example.com\", body, opts...) {\n\t// do something\n}\n```\n\n#### Delete\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n\tr2.WithContentType(r2.ContentTypeApplicationJson),\n}\nbody := bytes.NewBuffer([]byte(`{\"foo\": \"bar\"}`))\nfor res, err := range r2.Delete(ctx, \"https://example.com\", body, opts...) {\n\t// do something\n}\n```\n\n#### PostForm\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n\tr2.WithContentType(r2.ContentTypeApplicationJson),\n}\nform := url.Values{\"foo\": []string{\"bar\"}}\nfor res, err := range r2.Post(ctx, \"https://example.com\", form, opts...) {\n\t// do something\n}\n```\n\n#### Do\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n\tr2.WithPeriod(time.Second),\n\tr2.WithContentType(r2.ContentTypeApplicationJson),\n}\nbody := bytes.NewBuffer([]byte(`{\"foo\": \"bar\"}`))\nfor res, err := range r2.Do(ctx, http,MethodPost, \"https://example.com\", body, opts...) {\n\t// do something\n}\n```\n\n#### Termination Conditions\n\n- Request succeeded and no termination condition is specified by `WithTerminateIf`.\n- Condition that specified in `WithTerminateIf` is satisfied.\n- Response status code is a `4xx Client Error` other than `429: Too Many Request`.\n- Maximum number of requests specified in `WithMaxRequestAttempts` is reached.\n- Exceeds the deadline for the `context.Context` passed in the argument.\n- When the for range loop is interrupted by break.\n\n\n### Options\n\n**r2** provides the following request options\n\n| Option                                                                                                | Description                                                                                                                                                                                                               | Default              |\n|-------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------|\n| [`WithMaxRequestAttempts`](https://github.com/miyamo2/r2?tab=readme-ov-file#withmaxrequesttimes)      | The maximum number of requests to be performed.\u003c/br\u003eIf less than or equal to 0 is specified, maximum number of requests does not apply.                                                                                   | `0`                  |\n| [`WithPeriod`](https://github.com/miyamo2/r2?tab=readme-ov-file#withperiod)                           | The timeout period of the per request.\u003c/br\u003eIf less than or equal to 0 is specified, the timeout period does not apply. \u003c/br\u003eIf `http.Client.Timeout` is set, the shorter one is applied.                                  | `0`                  |\n| [`WithInterval`](https://github.com/miyamo2/r2?tab=readme-ov-file#withinterval)                       | The interval between next request.\u003c/br\u003eBy default, the interval is calculated by the exponential backoff and jitter.\u003c/br\u003eIf response status code is 429(Too Many Request), the interval conforms to 'Retry-After' header. | `0`                  |\n| [`WithTerminateIf`](https://github.com/miyamo2/r2?tab=readme-ov-file#withterminateif)                 | The termination condition of the iterator that references the response.                                                                                                                                                   | `nil`                |\n| [`WithHttpClient`](https://github.com/miyamo2/r2?tab=readme-ov-file#withhttpclient)                   | The client to use for requests.                                                                                                                                                                                           | `http.DefaultClient` |\n| [`WithHeader`](https://github.com/miyamo2/r2?tab=readme-ov-file#withheader)                           | The custom http headers for the request.                                                                                                                                                                                  | `http.Header`(blank) |\n| [`WithContentType`](https://github.com/miyamo2/r2?tab=readme-ov-file#withcontenttype)                 | The 'Content-Type' for the request.                                                                                                                                                                                       | `''`                 |\n| [`WithAspect`](https://github.com/miyamo2/r2?tab=readme-ov-file#withaspect)                           | The behavior to the pre-request/post-request.                                                                                                                                                                             | -                    |\n| [`WithAutoCloseResponseBody`](https://github.com/miyamo2/r2?tab=readme-ov-file#withautocloseresponse) | Whether the response body is automatically closed.\u003c/br\u003eBy default, this setting is enabled.                                                                                                                               | `true`               |\n\n#### WithMaxRequestAttempts\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithMaxRequestAttempts(3),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### WithPeriod\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithPeriod(time.Second),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### WithInterval\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithInterval(time.Second),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### WithTerminateIf\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithTerminateIf(func(res *http.Response, _ error) bool {\n\t\tmyHeader := res.Header.Get(\"X-My-Header\")\n\t\treturn len(myHeader) \u003e 0\n\t}),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### WithHttpClient\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nvar myHttpClient *http.Client = getMyHttpClient()\nopts := []r2.Option{\n\tr2.WithHttpClient(myHttpClient),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### WithHeader\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithHeader(http.Header{\"X-My-Header\": []string{\"my-value\"}}),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### WithContentType\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n\tr2.WithContentType(\"application/json\"),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n\t// do something\n}\n```\n\n#### WithAspect\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n    r2.WithAspect(func(req *http.Request, do func(req *http.Request) (*http.Response, error)) (*http.Response, error) {\n        res, err := do(req)\n        res.StatusCode += 1\n        return res, err\n    }),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n    // do something\n}\n```\n\n#### WithAutoCloseResponseBody\n\n```go\nctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\ndefer cancel()\nopts := []r2.Option{\n    r2.WithAutoCloseResponseBody(true),\n}\nfor res, err := range r2.Get(ctx, \"https://example.com\", opts...) {\n    // do something\n}\n```\n\n### Advanced Usage\n\n[Read more advanced usages](https://github.com/miyamo2/r2/blob/main/.doc/ADVANCED_USAGE.md)\n\n## For Contributors\n\nFeel free to open a PR or an Issue.  \nHowever, you must promise to follow our [Code of Conduct](https://github.com/miyamo2/r2/blob/main/CODE_OF_CONDUCT.md).\n\n### Tree\n\n```sh\n.\n├ .doc/            # Documentation\n├ .github/\n│    └ workflows/  # GitHub Actions Workflow\n├ internal/        # Internal Package; Shared with sub-packages.\n└ tests/            \n    ├ integration/ # Integration Test\n    └ unit/        # Unit Test\n```\n\n### Tasks\n\nWe recommend that this section be run with [`xc`](https://github.com/joerdav/xc).\n\n#### setup:deps\n\nInstall `mockgen` and `golangci-lint`.\n\n```sh\ngo install go.uber.org/mock/mockgen@latest\ngo install github.com/golangci/golangci-lint/cmd/golangci-lint@latest\n```\n\n#### setup:goenv\n\nSet `GOEXPERIMENT` to `rangefunc` if Go version is 1.22.\n\n```sh\nGOVER=$(go mod graph)\nif [[ $GOVER == *\"go@1.22\"* ]]; then\n  go env -w GOEXPERIMENT=rangefunc\nfi\n```\n\n#### setup:mocks\n\nGenerate mock files.\n\n```sh\ngo mod tidy\ngo generate ./...\n```\n\n#### lint\n\n```sh\ngolangci-lint run --fix\n```\n\n#### test:unit\n\nRun Unit Test\n\n```sh\ncd ./tests/unit\ngo test -v -coverpkg=github.com/miyamo2/r2 ./... -coverprofile=coverage.out \n```\n\n#### test:integration\n\nRun Integration Test\n\n```sh\ncd ./tests/integration\ngo test -v -coverpkg=github.com/miyamo2/r2 ./... -coverprofile=coverage.out \n```\n\n## License\n\n**r2** released under the [MIT License](https://github.com/miyamo2/r2/blob/main/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmiyamo2%2Fr2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmiyamo2%2Fr2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmiyamo2%2Fr2/lists"}