{"id":13990875,"url":"https://github.com/ijpiantanida/talkback","last_synced_at":"2026-01-14T20:44:47.864Z","repository":{"id":25241677,"uuid":"103073370","full_name":"ijpiantanida/talkback","owner":"ijpiantanida","description":"A simple HTTP proxy that records and playbacks requests","archived":false,"fork":false,"pushed_at":"2024-08-31T15:05:37.000Z","size":1315,"stargazers_count":302,"open_issues_count":8,"forks_count":41,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-08-09T17:15:33.568Z","etag":null,"topics":["http-proxy","http-recording","playback","proxy"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/ijpiantanida.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"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":"2017-09-11T00:54:27.000Z","updated_at":"2025-08-06T23:56:20.000Z","dependencies_parsed_at":"2023-12-18T03:27:31.811Z","dependency_job_id":"b2252625-8c8b-4d5b-8376-cf190fecadf3","html_url":"https://github.com/ijpiantanida/talkback","commit_stats":{"total_commits":185,"total_committers":13,"mean_commits":14.23076923076923,"dds":0.0972972972972973,"last_synced_commit":"ac043cf8f5d333d80fb832c0fc2bb9ea13fc5d95"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/ijpiantanida/talkback","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ijpiantanida%2Ftalkback","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ijpiantanida%2Ftalkback/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ijpiantanida%2Ftalkback/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ijpiantanida%2Ftalkback/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ijpiantanida","download_url":"https://codeload.github.com/ijpiantanida/talkback/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ijpiantanida%2Ftalkback/sbom","scorecard":{"id":483442,"data":{"date":"2025-08-11","repo":{"name":"github.com/ijpiantanida/talkback","commit":"2f4cf8a693b529498de5b4fbf7a6f035bc15a172"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.2,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 2/27 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Info: jobLevel 'actions' permission set to 'read': .github/workflows/codeql-analysis.yml:32","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql-analysis.yml:33","Warn: no topLevel permission defined: .github/workflows/ci_build.yml:1","Warn: no topLevel permission defined: .github/workflows/codeql-analysis.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_build.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/ci_build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_build.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/ci_build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_build.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/ci_build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_build.yml:32: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/ci_build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_build.yml:42: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/ci_build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_build.yml:46: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/ci_build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_build.yml:51: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/ci_build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:37: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:48: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:62: update your workflow using https://app.stepsecurity.io/secureworkflow/ijpiantanida/talkback/codeql-analysis.yml/main?enable=pin","Info:   0 out of  11 GitHub-owned GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.md:0","Info: FSF or OSI recognized license: MIT License: LICENSE.md:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":7,"reason":"SAST tool detected but not run on all commits","details":["Info: SAST configuration detected: CodeQL","Warn: 0 commits out of 5 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"11 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-pq67-2wwv-3xjx","Warn: Project is vulnerable to: GHSA-8cj5-5rvv-wf4v","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-76p7-773f-r4q5","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-19T17:12:02.411Z","repository_id":25241677,"created_at":"2025-08-19T17:12:02.411Z","updated_at":"2025-08-19T17:12:02.411Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28434497,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T18:57:19.464Z","status":"ssl_error","status_checked_at":"2026-01-14T18:52:48.501Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["http-proxy","http-recording","playback","proxy"],"created_at":"2024-08-09T13:03:26.605Z","updated_at":"2026-01-14T20:44:47.845Z","avatar_url":"https://github.com/ijpiantanida.png","language":"TypeScript","funding_links":[],"categories":["TypeScript","proxy"],"sub_categories":[],"readme":"\u003cimg src=\"docs/logo.png\" width=\"300\" alt=\"Talkback logo\"\u003e\n\n# Talkback\n\nTalkback is a javascript HTTP proxy that records and playbacks HTTP requests. As long as you have node.js in your environment, you can run talkback to record requests from applications written in any language/framework.   \nYou can use it to accelerate your integration tests or run your application against a mocked server.       \n\n[![npm version](https://badge.fury.io/js/talkback.svg)](https://badge.fury.io/js/talkback)\n[![Build Status](https://github.com/ijpiantanida/talkback/actions/workflows/ci_build.yml/badge.svg?branch=main)](https://github.com/ijpiantanida/talkback/actions/workflows/ci_build.yml)\n\n## Installation\n\n```\nnpm install talkback\n```\n\n## Usage\n\nTalkback is pretty easy to set up.   \nDefine which host it will be proxying, which port it should listen to and where to find and save tapes.   \n\nWhen a request arrives to talkback, it will try to match it against a previously saved tape and quickly return the tape's response.   \nIf no tape matches the request, it will forward it to the origin host, save the tape to disk for future uses and return the response.   \n\n```typescript\nconst talkback = require(\"talkback\");\n//import talkback from \"talkback/es6\";\n\nconst opts = {\n  host: \"https://api.myapp.com/foo\",\n  record: talkback.Options.RecordMode.NEW,\n  port: 5544,\n  path: \"./my-tapes\"\n};\nconst server = talkback(opts);\nserver.start(() =\u003e console.log(\"Talkback Started\"));\n```\n\nTalkback can be used in 2 ways:\n  - as a standalone HTTP server in its own separate process. [Example](/examples/server).\n  - as a library, where you are in charge of routing requests to talkback. [Example](/examples/request-handler).\n\n#### talkback(options: Partial\\\u003cOptions\\\u003e): TalkbackServer\nReturns an unstarted instance of a talkback server.   \nSee all [Options](#options).\n\n```typescript\nconst talkback = talkback(options)\n\ntalkback.start(() =\u003e console.log(\"Talkback Started\"))\ntalkback.close()\n```\n\n#### talkback.requestHandler(options: Partial\\\u003cOptions\\\u003e): Promise\\\u003cRequestHandler\\\u003e\nReturns a RequestHandler instance ready to receive requests.   \nSee all [Options](#options).   \n\nThe handler takes a [request](#request) and returns a [response](#response) Promise.\n\n```typescript\nconst talkbackHandler = await talkback.requestHandler(options)\n\nconst response = await talkbackHandler.handle(httpRequest)\n```\n\n### Options\n\n| Name | Type | Description | Default |   \n|------|------|-------------|---------|\n| **host** | `String` | Where to proxy unknown requests| |\n| **port** | `String` | Talkback port | `8080` |\n| **path** | `String` | Path where to load and save tapes | `./tapes/` |\n| **https** | `Object` | HTTPS server [options](#https-options) | [Defaults](#https-options) |\n| **httpClient** | `Object` | Customize the options used in the HTTP call when proxying requests. [More info](#http-client-options) | [Defaults](#http-client-options) |\n| **record** | `String \\| Function` | Set record mode. [More info](#recording-modes) | `RecordMode.NEW` |\n| **fallbackMode** | `String \\| Function` | Fallback mode for unknown requests when recording is disabled. [More info](#recording-modes) | `FallbackMode.NOT_FOUND` |\n| **name** | `String` | Server name | Defaults to `host` value |\n| **tapeNameGenerator** | `Function` | [Customize](#file-name) how a tape name is generated for new tapes. | `null` |\n| **allowHeaders** | `[String]` | List of headers to include when matching tapes. If present, headers that are not part of the list will be ignored. By default, most headers are considered (See `ignoreHeaders`)\u003c/br\u003e\u003c/br\u003eSetting this value to `[]` will disable header matching on tapes.\u003c/br\u003eNote that `content-type` and `content-encoding` are needed to decode the body into plain-text. [More info](#request-and-response-body)  | `null` |\n| **ignoreHeaders** | `[String]` | List of headers to ignore when matching tapes. By default, most headers are considered | `['content-length', 'host']` |\n| **ignoreQueryParams** | `[String]` | List of query params to ignore when matching tapes. Useful when having dynamic query params like timestamps| `[]` |\n| **ignoreBody** | `Boolean` | Should the request body be ignored when matching tapes | `false` |\n| **bodyMatcher** | `Function` | Customize how a request's body is matched against saved tapes. [More info](#custom-request-body-matcher) | `null` |\n| **urlMatcher** | `Function` | Customize how a request's URL is matched against saved tapes. [More info](#custom-request-url-matcher) | `null` |\n| **requestDecorator** | `Function` | Modify requests before they are proxied. [More info](#custom-request-decorator) | `null` |  \n| **responseDecorator** | `Function` | Modify responses before they are returned. [More info](#custom-response-decorator) | `null` |  \n| **tapeDecorator** | `Function` | Modify tapes before they are stored. [More info](#custom-tape-decorator) | `null` |\n| **latency** | `Number \\| \\[Number\\] \\| Function` | Synthetic latency for requests (in ms). [More info](#latency) | `0` |\n| **errorRate** | `Number \\| Function` | Probability between 0 and 100 of injecting a synthetic error. [More info](#error-rate) | `0` |\n| **silent** | `Boolean` | Disable requests information console messages in the middle of requests | `false` |\n| **summary** | `Boolean` | Enable exit summary of new and unused tapes at exit. [More info](#exit-summary) | `true` |\n| **debug** | `Boolean` | Enable verbose debug information | `false` |\n\n### HTTPS options\n| Name | Type | Description | Default |\n|------|------|-------------|---------|\n| **enabled** | `Boolean` | Enables HTTPS server | `false` |\n| **keyPath** | `String` | Path to the key file | `null` | \n| **certPath** | `String` | Path to the cert file | `null` | \n\n### HTTP client options\n| Name | Type | Description | Default |\n|------|------|-------------|---------|\n| **fetchOptions** | `Object` | Additional options passed to the fetch (`node-fetch`) call. [List of supported options](https://www.npmjs.com/package/node-fetch#options). | `{}` |\n\n#### Example: Using a proxy\nYou can pass a custom `agent` to the fetch call, for example to use a proxy.\n```typescript\nimport { HttpsProxyAgent } from 'https-proxy-agent';\n\nconst agent = new HttpsProxyAgent('http://proxy.example.com:3128');\n\nconst talkbackOpts = {\n  ...,\n  httpClient: {\n    fetchOptions: {\n      agent\n    }\n  },\n}\n```\n\n\n## Tapes\nTapes are where talkback stores requests and their response.   \n* They can be freely edited to match new requests or return a different response than the original. They are loaded recursively from the `path` directory at startup. Since they are only loaded on startup, any changes to a tape requires a server restart to be applied.   \n* Talkback will do a best effort to store the tape request and response body in plain text (human readable) [More info](#request-and-response-body).    \n* Tapes use the [JSON5](http://json5.org/) format. JSON5 is an extensions to the JSON format that allows for very neat features like comments, trailing commas and keys without quotes.      \n\n#### Format\nAll tapes have the following 3 properties:   \n* **meta**: [Metadata](#metadata) object. Stores additional metadata about the tape.\n* **req**: [Request](#request) object. Used to match incoming requests against the tape.\n* **res**: [Response](#response) object. The HTTP response that will be returned in case the tape matches a request.\n\n#### Metadata\n| Property | Type | Description | Example |\n|----------|------|-------------|---------|\n| **createdAt** | `Date` | Creation datetime of the tape | `2018-12-07T02:49:53.859Z` |\n| **host** | `String` | Base host url used for this request. Informative, it plays no role during the matching process | `https://api.github.com` |\n| **tag** | `String` | Custom tag to identify the tape | `auth` |\n| **errorRate** | `Number` | Number between 0 and 100 that marks the probability of the request producing a synthetic failure. [More info](#error-rate) | `10` |\n| **latency** | `Number \\| [Number]` | Synthetic latency for requests (in ms). [More info](#latency) | `10` |\n| **reqUncompressed** | `Boolean` | Whether the request body has been uncompressed | `false` |\n| **resUncompressed** | `Boolean` | Whether the response body has been uncompressed | `false` | \n| **reqHumanReadable** | `Boolean` | Whether the request body is in a human-readable format or base64 encoded | `true` | \n| **resHumanReadable** | `Boolean` | Whether the response body is in a human-readable format or base64 encoded | `true` |\n   \nIn addition to talkback properties, you can define their own custom fields either by manually editing the tape file or by dynamically adding them using a [custom tape decorator](#custom-tape-decorator).   \n\n#### Request\n| Property | Type | Description | Example |\n|----------|------|-------------|---------|\n| **url** | `String` | Url relative to the host. | `/users` |\n| **method** | `String` | HTTP method | `GET` |\n| **headers** | `Object\\\u003cString, String\\\u003e` | Request headers | `{\"content-type\": \"application/json\", accept: \"*/*\"}` |\n| **body** | `Buffer` | Request body | `Buffer.from(\"FOOBAR\")` |\n\n#### Response\n| Property | Type | Description | Example |\n|----------|------|-------------|---------|\n| **status** | `Number` | HTTP response status code | `200` |\n| **headers** | `Object\\\u003cString, [String]\\\u003e` | Response headers | `{\"content-type\": [\"application/json\"]}` |\n| **body** | `Buffer` | Response body | `Buffer.from(\"FOOBAR\")` |\n\n#### Request and Response body\nTalkback will store the request and response body in plan text and uncompressed (human readable) if the `content-encoding` is supported (gzip, deflate, br) and the `content-type` is considered human readable ([see list](src/utils/media-type.ts#L15)).\u003c/br\u003e\nFor this to work, both headers should be present in the request/response. Keep this in mind when setting the `allowHeaders` and `ignoreHeaders` options.  \n\n##### Pretty Printing\nIf the request or response have a JSON *content-type*, their body will be pretty printed as an object in the tape for easier readability.   \nThis means differences in formatting are ignored when comparing tapes, and any special formatting in the response will be lost.\n\n#### File Name\nBy default, new tapes will be created under the `path` directory with the name `unnamed-[created_at_ms].json5` (`unnamed-1715145165683.json5`).\nTapes can be renamed at will, for example to give some meaning to the scenario the tape represents.  \nA custom `tapeNameGenerator` function can be provided to generate the file path (relative to `path`) at which to store the tape, using the tape's content.\nNote that the file extension `.json5` will be appended automatically.\n\n##### Example:\n```typescript\nfunction nameGenerator(tapeEpoch: number, tape: Tape) {\n  // organize in folders by request method\n  // e.g. tapes/GET/unnamed-1715145165683.json5\n  //      tapes/GET/unnamed-1715145207909.json5\n  //      tapes/POST/unnamed-1715145207946.json5\n  return path.join(`${tape.req.method}`, `unnamed-${tapeEpoch}`)\n}\n``` \n \n## Recording Modes\nTalkback proxying and recording behavior can be controlled through the `record` and `fallbackMode` options.   \n\nThere are 3 possible recording modes:   \n\n| Value | Description |\n|-------|-------------|\n| `NEW` | If no tape matches the request, proxy it and save the response to a tape |\n| `OVERWRITE` | Always proxy the request and save the response to a tape, overwriting any existing one |\n| `DISABLED` | If a matching tape exists, return it. Otherwise, don't proxy the request and use `fallbackMode` for the response |\n            \nThe `fallbackMode` option lets you choose what to do when recording is `DISABLED` and an unknown request arrives.  \n\nThere are 2 possible fallback modes:   \n\n| Value | Description |\n|-------|-------------|\n| `NOT_FOUND` | Log an error and return a 404 response |\n| `PROXY` | Proxy the request to `host` and return its response, but don't create a tape |\n\n**It is recommended to `DISABLE` recording when using talkback for test running. This way, there are no side effects and broken tests fail faster.**\n\nBoth options accept either one of the possible modes to be used for all requests or a function that takes the request as a parameter and returns a valid mode.\n\n```typescript\nconst talkback = require(\"talkback\")\n\nconst opts = {\n  record: talkback.Options.RecordMode.DISABLED,\n  fallbackMode: (req: Req) =\u003e {\n    if (req.url.includes(\"/mytest\")) {\n        return talkback.Options.FallbackMode.PROXY\n      }\n      return talkback.Options.FallbackMode.NOT_FOUND\n  } \n}\n```\n\n## Custom request body matcher\nBy default, in order for a request to match against a saved tape, both request and tape need to have the exact same body.      \nThere might be cases where this rule is too strict (for example, if your body contains time dependent bits) but enabling `ignoreBody` is too lax.\n\nTalkback lets you pass a custom matching function as the `bodyMatcher` option.   \nThe function will receive a saved tape and the current request, and it has to return whether they should be considered a match on their body.   \nBody matching is the last step when matching a tape. In order for this function to be called, everything else about the request should match the tape too (url, method, headers).   \nThe `bodyMatcher` is not called if tape and request bodies are already the same. \n\n### Example:\n\n```typescript\nfunction bodyMatcher(tape: Tape, req: Req) {\n    if (tape.meta.tag === \"fake-post\") {\n      const tapeBody = JSON.parse(tape.req.body.toString());\n      const reqBody = JSON.parse(req.body.toString());\n\n      return tapeBody.username === reqBody.username;\n    }\n    return false;\n}\n```\n\nIn this case we are adding our own `tag` property to the saved tape `meta` object. This way, we are only using the custom matching logic on some specific requests, and can even have different logic for different categories of requests.   \nNote that both the tape's and the request's bodies are `Buffer` objects.\n\n## Custom request URL matcher\nSimilar to the [`bodyMatcher`](#custom-request-body-matcher) option, there's the `urlMatcher` option, which will let you customize how a request and a tape are matched on their URL.\n\n### Example:\n\n```typescript\nfunction urlMatcher(tape: Tape, req: Req) {\n    if (tape.meta.tag === \"user-info\") {\n      // Match if URL is of type /users/{username}\n      return !!req.url.match(/\\/users\\/[a-zA-Z-0-9]+/);\n    }\n    return false;\n}\n```\n\n## Custom decorators\nTalback lets you tap into the request lifecycle through the decorator options:\n1. [Request Decorator](#custom-request-decorator)\n2. [Response Decorator](#custom-response-decorator)\n3. [Tape Decorator](#custom-tape-decorator)\n\n#### Matching Context\nWhen the request starts, talkback will create a `MatchingContext` object, which will be passed to all your decorators as an additional parameter.   \nIt's the main way in which you can connect all your different decorator functions without having to modify the actual request/response.   \n\nThe context will contain some useful properties, but you can also extend it with your own.\n\n|Property | Type | Description | Example | \n|---------|------|-------------|---------|\n| **id** | `String` | Unique id (UUID v4) | `52a3cdf9-e3be-439f-b81d-4301b4f5adf0` |\n\n## Custom request decorator\nBy default, talkback will just proxy requests to the host as they are.   \nIf you want to customize requests before they're proxied (or looked up in stored tapes) you can do so through the `requestDecorator` option.   \n\n`requestDecorator` takes a function that will receive the original request and the context object as parameters, and should return the modified request.\n\n```typescript\nfunction requestDecorator(req: Req, context: MatchingContext) {\n  requestStartTime[context.id] = new Date().getTime()\n\n  delete req.headers['accept-encoding'];\n  return req;\n}\n```\n\nIn this example we are using the context's id to store the request's start time to later be used by another decorator. \n\n  \n## Custom response decorator\nIf you want to add dynamism to the response coming from a matching existing tape or adjust the response that the proxied server returns, you can do so by using the `responseDecorator` option.      \nThis can be useful for example if your response needs to contain an ID that gets sent on the request, or if your response has a time dependent field.     \n\nThe function will receive a copy of the matching tape, the in-flight request object and the context object as parameters, and it should return the modified tape. Note that since you're receiving a copy of the matching tape, changes to the object won't persist between different requests.   \nTalkback will also update the `Content-Length` header if it was present in the original response.   \n\n### Example:\nWe're going to hit an `/auth` endpoint, and update just the `expiration` field of the JSON response that was saved in the tape to be a day from now.      \n\n```typescript\nfunction responseDecorator(tape: Tape, req: Req, context: MatchingContext) {\n  if (tape.meta.tag === \"auth\") {\n    const tapeBody = JSON.parse(tape.res.body.toString())\n    const expiration = new Date()\n    expiration.setDate(expiration.getDate() + 1)\n    const expirationEpoch = Math.floor(expiration.getTime() / 1000)\n    tapeBody.expiration = expirationEpoch\n\n    const newBody = JSON.stringify(tapeBody)\n    tape.res.body = Buffer.from(newBody)\n  }\n  return tape\n}\n```\n\nIn this example we are making use of the `meta.tag` property on the saved tape to decide whether we apply the custom logic or not.      \n*Note that both the tape's and the request's bodies are `Buffer` objects, and they should be kept as such.*    \n\n## Custom tape decorator\nBefore saving the tape to disk talback can call your own `tapeDecorator` function where you can edit any of the tape's properties.\nThe function will receive the original tape and the context object as parameters, and it should return the tape to be stored.   \n\nYou can use this to edit any of talkback's properties or add your own `meta` fields.\n\n```typescript\nfunction tapeDecorator(tape: Tape, context: MatchingContext) {\n  if (tape.req.url.includes(\"/auth/\")) {\n    tape.meta.tag = \"auth\"\n  }\n\n  const originalDurationMs = new Date().getTime() - requestStartTime[context.id]\n  tape.meta.originalDurationMs = originalDurationMs \n  tape.meta.latency = [Math.floor(0.5*originalDurationMs), Math.floor(1.5*originalDurationMs)]\n  \n  return tape\n}\n```\nIn this example we are dynamically adding a tag based on the request URL.  \nWe are also using the context's id to retrieve the initial request time which was saved by a custom [`requestDecorator`](#custom-request-decorator) and calculating how long did the request take. We store the original duration as a custom meta property, and we also set a range for the [`latency` feature](#latency).  \n\n## Latency\nBy default, talkback will try to reply to requests as fast as it can, but sometimes it's useful to understand how applications behave under real-world or even undesirably high response times.   \nTalkback lets you control response times both at a _global_ or at a _tape level_.   \n\nThe `latency` option will apply for all requests that match an existing tape or when using the [`PROXY` fallback mode](#recording-modes).   \n\nThere are 3 possible types of values:\n - A number: Fixed number of milliseconds for all response times.\n - An array in the form `[min, max]`: Requests will take a random number of milliseconds in the given range.\n - A function `(req) =\u003e latency`: The function will be called for each request and it should return the desired number of milliseconds for the response time.\n\nAt the same time, tapes can define their own specific response times by adding a `latency` property to the [`meta` object](#tapes).   \nThis property accepts both numbers and ranges and will take precedence over the global `latency` option.\n\n```json\n{\n  \"meta\": {\n    \"createdAt\": \"2017-09-10T23:19:27.010Z\",\n    \"host\": \"http://localhost:8898\",\n    \"resHumanReadable\": true,\n    \"latency\": [100, 500]\n  },\n  ...\n}\n```\n\n## Error rate\nSimilar to what the [latency](#latency) option does, you might want to test how your application behaves when downstream services start failing.   \nTalkback can aid here through the `errorRate` option, by returning synthetic 503 errors back to you application.\n \nThe `errorRate` option will apply for all requests that match an existing tape or when using the [`PROXY` fallback mode](#recording-modes).   \n\nThere are 2 possible types of values:\n - A number between 0 and 100 that defines the probability of returning an error for each request.\n - A function `(req) =\u003e errorRate`: The function will be called for each request and it should return the desired probability of error for that specific request.\n\nAt the same time, tapes can define their own specific error rates by adding an `errorRate` property to the [`meta` object](#tapes).   \n\n```json\n{\n  \"meta\": {\n    \"createdAt\": \"2017-09-10T23:19:27.010Z\",\n    \"host\": \"http://localhost:8898\",\n    \"resHumanReadable\": true,\n    \"errorRate\": 50\n  },\n  ...\n}\n```\n\n## Exit summary\nIf you are using talkback for your test suite, you will probably have tons of different tapes after some time. It can be difficult to know if all of them are still required.   \nTo help, when talkback exits, it will print a list of all the tapes that have NOT been used and a list of all the new tapes. If your test suite is green, you can safely delete anything that hasn't been used.\n\n```\n===== SUMMARY (My Server) =====\nNew tapes:\n- unnamed-1715145207909.json5\nUnused tapes:\n- not-valid-request.json5\n- user-profile.json5\n```\n\nThis can be disabled with the `summary` option.\n\n# Licence\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fijpiantanida%2Ftalkback","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fijpiantanida%2Ftalkback","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fijpiantanida%2Ftalkback/lists"}