{"id":13630397,"url":"https://github.com/zalando-incubator/perron","last_synced_at":"2025-04-17T13:32:16.676Z","repository":{"id":8869200,"uuid":"60007312","full_name":"zalando-incubator/perron","owner":"zalando-incubator","description":"A sane node.js client for web services","archived":true,"fork":false,"pushed_at":"2024-06-28T08:18:55.000Z","size":562,"stargazers_count":44,"open_issues_count":10,"forks_count":23,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-14T10:21:14.701Z","etag":null,"topics":["circuit-breaker","http","javascript","nodejs","request","services","web"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zalando-incubator.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-05-30T12:07:43.000Z","updated_at":"2024-06-28T10:42:56.000Z","dependencies_parsed_at":"2023-10-11T12:12:44.573Z","dependency_job_id":"3d6af4ad-76b2-4c17-a7a9-dad53ae78026","html_url":"https://github.com/zalando-incubator/perron","commit_stats":{"total_commits":169,"total_committers":26,"mean_commits":6.5,"dds":0.6982248520710059,"last_synced_commit":"6a5c0ab38f3c6aa5027007d40637815568764608"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalando-incubator%2Fperron","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalando-incubator%2Fperron/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalando-incubator%2Fperron/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalando-incubator%2Fperron/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zalando-incubator","download_url":"https://codeload.github.com/zalando-incubator/perron/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249267733,"owners_count":21240851,"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":["circuit-breaker","http","javascript","nodejs","request","services","web"],"created_at":"2024-08-01T22:01:41.223Z","updated_at":"2025-04-17T13:32:16.406Z","avatar_url":"https://github.com/zalando-incubator.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# ATTENTION! This project is not supported anymore.\n\n[![Build Status](https://github.com/zalando-incubator/perron/workflows/CI/badge.svg?branch=master)](https://github.com/zalando-incubator/perron/actions)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n# Perron\n\nA sane client for web services with a built-in circuit-breaker, support for filtering both request and response.\n```\nnpm install perron --save\n```\n\n**[Changelog](https://github.com/zalando-incubator/perron/blob/master/CHANGELOG.md)**\n\n## Quick Example\n\nThe following is a minimal example of using `perron` to call a web service:\n\n```js\nconst {ServiceClient} = require('perron');\n\n// A separate instance of `perron` is required per host\nconst catWatch = new ServiceClient('https://catwatch.opensource.zalan.do');\n\ncatWatch.request({\n    pathname: '/projects',\n    query: {\n        limit: 10\n    }\n}).then(data =\u003e console.log(data));\n```\n\n## Making Requests\n\nEach call to `request` method will return a Promise, that would either resolve to a successful `ServiceClient.Response` containing following fields:\n\n```js\n{\n    statusCode, // same as Node IncomingMessage.statusCode\n    headers, // same as Node IncomingMessage.headers\n    body // if JSON, then parsed body, otherwise a string\n}\n```\n\n`request` method accepts an objects with all of the same properties as [https.request](https://nodejs.org/api/https.html#https_https_request_options_callback) method in Node.js, except from a `hostname` field, which is taken from the options passed when creating an instance of `ServiceClient`. Additionally you can add a `timeout` and `readTimeout` fields, which define time spans in ms for socket connection and read timeouts.\n\n## Handling Errors\n\nFor the error case you will get a custom error type `ServiceClientError`. A custom type is useful in case you change the request to some other processing in your app and then need to distinguish between your app error and requests errors in a final catch.\n\n`ServiceClientError` class contains an optional `response` field that is available if any response was received before there was an error.\n\nIf you have not specified retry options, you can use `instanceof` checks on the error to determine exact reason:\n\n```js\ncatWatch.request({\n  path: '/projects?limit=10'\n}).then(console.log, logError);\n\nfunction logError(err) {\n  if (err instanceof BodyParseError) {\n    console.log('Got a JSON response but parsing it failed');\n    console.log('Raw response was', err.response);\n  } else if (err instanceof RequestFilterError) {\n    console.log('Request filter failed');\n  } else if (err instanceof ResponseFilterError) {\n    console.log('Response filter failed');\n    console.log('Raw response was', err.response);\n  } else if (err instanceof CircuitOpenError) {\n    console.log('Circuit breaker is open');\n  } else if (err instanceof RequestConnectionTimeoutError) {\n    console.log('Connection timeout');\n    console.log('Request options were', err.requestOptions);\n  } else if (err instanceof RequestReadTimeoutError) {\n    console.log('Socket read timeout');\n    console.log('Request options were', err.requestOptions);\n  } else if (err instanceof RequestUserTimeoutError) {\n    console.log('Request dropped after timeout specified in `dropRequestAfter` option');\n    console.log('Request options were', err.requestOptions);\n  } else if (err instanceof RequestNetworkError) {\n    console.log('Network error (socket, dns, etc.)');\n    console.log('Request options were', err.requestOptions);\n  } else if (err instanceof InternalError) {\n    // This error should not happen during normal operations\n    // and usually indicates a bug in perron or misconfiguration\n    console.log('Unknown internal error');\n  }\n}\n```\n\nIf you have retries configured, there are only 3 types of errors you will get that are relating to circuit breakers and retries, however you can access original errors that led to retries through `retryErrors` field available on both the successful response:\n\n```js\n\ncatWatch.request({\n  path: '/projects?limit=10'\n}).then(function (result) {\n  console.log(\"Response was\", result.body);\n  if (result.retryErrors.length) {\n    console.log(\"Request successful, but there were retries:\");\n    result.retryErrors.forEach(logError);\n  }\n}, logError);\n\nfunction logRetryError(err) {\n  if (err instanceof CircuitOpenError) {\n    console.log('Circuit breaker is open');\n  } else if (err instanceof ShouldRetryRejectedError) {\n    console.log('Provided `shouldRetry` function rejected retry attempt');\n    err.retryErrors.forEach(logError);\n  } else if (err instanceof MaximumRetriesReachedError) {\n    console.log('Reached maximum retry count');\n    err.retryErrors.forEach(logError);\n  }\n}\n```\n\n## Circuit Breaker\n\nIt's almost always a good idea to have a circuit breaker around your service calls, and generally one per hostname is also a good default since 5xx usually means something is wrong with the whole service and not a specific endpoint.\n\nThis is why `perron` by default includes one circuit breaker per instance. Internally `perron` uses [circuit-breaker-js](https://github.com/yammer/circuit-breaker-js), so you can use all of it's options when configuring the breaker:\n\n```js\nconst {ServiceClient} = require('perron');\n\nconst catWatch = new ServiceClient({\n    hostname: 'catwatch.opensource.zalan.do',\n    // If the \"circuitBreaker\" settings are passed (non-falsy), they will be merged\n    // with the default options below. Otherwise, circuit breaking will be disabled\n    circuitBreaker: {\n        windowDuration: 10000,\n        numBuckets: 10,\n        timeoutDuration: 2000,\n        errorThreshold: 50,\n        volumeThreshold: 10\n    }\n});\n```\n\nOptionally the `onCircuitOpen` and `onCircuitClose` functions can be passed to the circuitBreaker object in order to track the state of the circuit breaker via metrics or logging:\n\n```js\nconst catWatch = new ServiceClient({\n    hostname: 'catwatch.opensource.zalan.do',\n    circuitBreaker: {\n        windowDuration: 10000,\n        numBuckets: 10,\n        timeoutDuration: 2000,\n        errorThreshold: 50,\n        volumeThreshold: 10,\n        onCircuitOpen: (metrics) =\u003e {\n          console.log('Circuit breaker open', metrics);\n        },\n        onCircuitClose: (metrics) =\u003e {\n          console.log('Circuit breaker closed', metrics);\n        }\n    }\n});\n```\n\nCircuit breaker will count all errors, including the ones coming from filters, so it's generally better to do pre- and post- validation of your request outside of filter chain.\n\nIf this is not the desired behavior, or you are already using a circuit breaker, it's always possible to disable the built-in one:\n\n```js\nconst catWatch = new ServiceClient({\n    hostname: 'catwatch.opensource.zalan.do',\n    circuitBreaker: false\n});\n```\n\nIn case if you want perron to still use a circuit breaker but it has to be provided dynamically by your code on-demand you can pass `circuitBreaker` option as a function (make sure to _not_ create a circuit breaker for every request):\n\n```js\nconst catWatch = new ServiceClient({\n    hostname: 'catwatch.opensource.zalan.do',\n    circuitBreaker: function (request) {\n      return somePreviouslyConstructedCB;\n    }\n});\n```\n\n## Retry Logic\n\nFor application critical requests it can be a good idea to retry failed requests to the responsible services.\n\nOccasionally target server can have high latency for a short period of time, or in the case of a stack of servers, one server can be having issues\nand retrying the request will allow perron to attempt to access one of the other servers that currently aren't facing issues.\n\nBy default `perron` has retry logic implemented, but configured to perform 0 retries. Internally `perron` uses [node-retry](https://github.com/tim-kos/node-retry) to handle the retry logic and configuration. All of the existing options provided by `node-retry` can be passed via configuration options through `perron`.\n\nThere is a `shouldRetry` function which can be defined in any way by the consumer and is used in the try logic to determine whether to attempt the retries or not depending on the type of error and the original request object.\nIf the function returns true and the number of retries hasn't been exceeded, the request can be retried.\n\nThere is also an `onRetry` function which can be defined by the user of `perron`. This function is called every time a retry request will be triggered.\nIt is provided the current attempt index, the error that is causing the retry and the original request params.\n\nThe first time `onRetry` gets called, the value of currentAttempt will be 2. This is because the first initial request is counted as the first attempt, and the first retry attempted will then be the second request.\n\n```js\nconst {ServiceClient} = require('perron');\n\nconst catWatch = new ServiceClient({\n    hostname: 'catwatch.opensource.zalan.do',\n    retryOptions: {\n        retries: 1,\n        factor: 2,\n        minTimeout: 200,\n        maxTimeout: 400,\n        randomize: true,\n        shouldRetry(err, req) {\n            return (err \u0026\u0026 err.response \u0026\u0026 err.response.statusCode \u003e= 500);\n        },\n        onRetry(currentAttempt, err, req) {\n            console.log('Retry attempt #' + currentAttempt + ' for ' + req.path + ' due to ' + err);\n        }\n    }\n});\n```\n\n## Filters\n\nIt's quite often necessary to do some pre- or post-processing of the request. For this purpose `perron` implements a concept of filters, that are just an object with 2 optional methods: `request` and `response`.\n\nBy default, every instance of `perron` includes a `treat5xxAsError` filter, but you can specify which filters should be use by providing a `filters` options when constructing an instance. This options expects an array of filter object and is *not* automatically merged with the default ones, so be sure to use `concat` if you want to keep the default filters as well.\n\nThere aren't separate request and response filter chains, so given that we have filters `A`, `B` and `C` the request flow will look like this:\n\n```\nA.request ---\u003e B.request ---\u003e C.request ---|\n                                           V\n                                      HTTP Request\n                                           |\nA.response \u003c-- B.response \u003c-- C.response \u003c--\n```\n\nIf corresponding `request` or `response` method is missing in the filter, it is skipped, and the flow goes to the next one.\n\n### Modifying the Request\n\nLet's say that we want to inject a custom header of the request. This is really easy to do in a request filter:\n\n```js\nconst {ServiceClient} = require('perron');\n\n// A separate instance of ServiceClient is required per host\nconst catWatch = new ServiceClient({\n    hostname: 'catwatch.opensource.zalan.do',\n    filters: [{\n        request(request) {\n            // let's pretend to be AJAX\n            request.headers['x-requested-with'] = 'XMLHttpRequest';\n            return request;\n        }\n    }, ServiceClient.treat5xxAsError]\n});\n```\n\n### Resolving Request in a Filter\n\nSometimes it is necessary to pretend to have called the service without actually doing it. This could be useful for caching, and is also very easy to implement:\n\n```js\nconst {ServiceClient} = require('perron');\n\nconst getCache = require('./your-module-with-cache');\n\n// A separate instance of ServiceClient is required per host\nconst catWatch = new ServiceClient({\n    hostname: 'catwatch.opensource.zalan.do',\n    filters: [{\n        request(request) {\n            const body = getCache(request);\n            if (body) {\n                const headers = {};\n                const statusCode = 200;\n                return new ServiceClient.Response(\n                    statusCode, headers, body\n                );\n            }\n            return request;\n        }\n    }, ServiceClient.treat5xxAsError]\n});\n```\n\nIf the request is resolved in such a way, all of the pending filter in the request and response chain will be skipped, so the flow diagram will look like this:\n\n```\n   called     |    not called\n---------------------------------\n cacheFilter  |  B.treat5xxAsError\n      |       |\n      |       |    HTTP Request\n      V       |\n cacheFilter  |  B.treat5xxAsError\n```\n\n### Rejecting Request in a Filter\n\nIt is possible to reject the request both in request and response filters by throwing, or by returning a rejected Promise. Doing so will be picked up by the circuit breaker, so this behavior should be reserved by the cases where the service returns `5xx` error, or the response is completely invalid (e.g. invalid JSON).\n\n### JSON Parsing\n\nBy default Perron will try to parse JSON body if the `content-type` header is not set or\nit is specified as `application/json`. If you wish to disable this behavior you can use\n`autoParseJson: false` option when constructing `ServiceClient` object.\n\n### UTF-8 Decoding\nBy default Perron will try to decode JSON body to UTF-8 string.\nIf you wish to disable this behaviour, you can use `autoDecodeUtf8: false` option\nwhen calling `request` method.\n\n### Opentracing\n\nPerron accepts a Span like object where it will log the network and request related events.\n\n## License\n\nThe MIT License\n\nCopyright (c) 2016 Zalando SE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzalando-incubator%2Fperron","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzalando-incubator%2Fperron","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzalando-incubator%2Fperron/lists"}