{"id":21444882,"url":"https://github.com/reinert/requestor","last_synced_at":"2025-07-14T18:31:52.635Z","repository":{"id":23050307,"uuid":"26403452","full_name":"reinert/requestor","owner":"reinert","description":"Request like a boss. 😎","archived":false,"fork":false,"pushed_at":"2024-05-03T04:13:01.000Z","size":33572,"stargazers_count":61,"open_issues_count":8,"forks_count":8,"subscribers_count":9,"default_branch":"master","last_synced_at":"2024-05-04T10:36:47.042Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/reinert.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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":"2014-11-09T17:40:34.000Z","updated_at":"2024-05-03T04:09:57.000Z","dependencies_parsed_at":"2024-05-03T05:40:23.170Z","dependency_job_id":null,"html_url":"https://github.com/reinert/requestor","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reinert%2Frequestor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reinert%2Frequestor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reinert%2Frequestor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reinert%2Frequestor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reinert","download_url":"https://codeload.github.com/reinert/requestor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225991824,"owners_count":17556370,"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":[],"created_at":"2024-11-23T02:22:07.724Z","updated_at":"2024-11-23T02:22:14.668Z","avatar_url":"https://github.com/reinert.png","language":"Java","funding_links":[],"categories":["HTTP and REST Library","HTTP客户端"],"sub_categories":["微服务框架"],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://user-images.githubusercontent.com/1285494/158213729-52458234-6c6b-41d5-bd6d-746c12a7c2c4.png\" alt=\"Request like a boss.\"/\u003e\n\u003c/p\u003e\n\n## Why Requestor?\nIt uniquely combines simplicity, completeness and performance in a singular HTTP Client API for any Java derived language.\n\nRequestor is:\n* **Async-First** → smoothly handle several concurrent client-server interactions with the max throughput.\n* **Event-Driven** → narrow down your code to specific results and implement complex features like HTTP Polling and Streaming seamlessly.\n* **Session-Based** → set up multiple client configurations for different backends with instance level customization.\n* **Scope-Bounded** → sessions, services and requests have isolated and interconnected contexts with fine-grained control over them.\n* **Cache-Assisted** → promptly save and share data among different parts of your application while handling communication.\n* **Functional-Friendly** → the api is designed to allow you to leverage the most of java lambdas and functional programming.\n\n\n## How is it different from *Retrofit* and *Feign-Client*?\nThese are two of the most common choices regarding HTTP API consumption in the Java ecosystem.\nThey happen to have some similarities, though, that contrast with *Requestor* fundamentals.\nBoth were primarily designed to provide auto-generated clients for HTTP APIs declared as interfaces,\nstrongly biased by popular REST Server APIs, like JAX-RS, that work in the same fashion.\nThis elementary design decision can cause some issues:\n- **High initial friction**: we can be burdened with a bunch of declarations and configurations before starting to use the library. This is special harmful when all we need is just to make a couple of simples requests.\n- **Bad code traceability**: when all we have is auto-generated code, it may become difficult to find bugs and understand the code flow when needed. And this time will come.\n- **Limited customizability**: when things start getting complex, we may find ourselves adding an excessive amount of code to handle interdependent scenarios.\n\nDifferently, Requestor's core goal is to provide high-fidelity modeling of the HTTP concepts oriented by the client's perspective. In addition, a request-response processing cycle was designed to allow us to customize the requests and responses at specific milestones and notify other parts of the system.\nAll this asynchronously. Around it, third-part concepts like Sessions, Services, and Stores, were attached to provide enhanced functionality and empower a communication-centric approach to build apps.\n\nRequestor's design is extensible enough to allow us to exponentially grow the complexity of our requirements linearly affecting the size of our code, keeping it clean and dry.\nWith Requestor we can:\n- Start simple and **make one line requests**.\n- **Request in a sync or async flow**, although all requests are executed asynchronously (in background threads or coroutines), following the *thread-per-request* style, not blocking the main thread. Requestor is superpowered by the [Java Virtual Threads](#-how-to-use-virtual-threads-with-requestor-jdk19)!\n- Set actions for specific request results due to a tailor-made event system to the request-response lifecycle, helping us to **write clear and concise code**.\n- Straightforwardly enable complex features - such as polling, streaming, and retrying - and **build sophisticated communication flows** painlessly.\n- **Add async middlewares to requests and responses** at different milestones in a well-suited lifecycle.\n- Conveniently manipulate and interact with Headers, URIs, and Links to **navigate through discoverable HTTP APIs**, employing HATEOAS the right way.\n\n\n## Features\n* [**Requesting Fluent API**](#%EF%B8%8F-requesting-fluent-api-briefing) - code as you think, read as you code.\n* [**Event-Driven Callbacks**](#event-driven-callbacks) - set callbacks for different results in a precise event system.\n* [**Futures**](#futures) - access the response header, the body and the deserialized payload as soon they are available.\n* [**Await**](#await) - alternatively work in a synchronous fashion by waiting for the request to finish.\n* [**Serialization**](#serialization) - serialize and deserialize payloads integrating any library.\n* [**Authentication**](#authentication) - make complex async authentication procedures in a breeze.\n* [**Middlewares**](#processors-middlewares) - asynchronously filter and intercept requests and responses.\n* [**HTTP Polling**](#poll) - make long or short polling with a single command.\n* [**HTTP Streaming**](#http-streaming) - efficiently stream the byte chunks as soon they are received.\n* [**Retry Policy**](#retry) - easily define a retry policy with two alternative approaches.\n* [**Session**](#session) - manage all requesting configurations in one place.\n* [**Service**](#services) - break down the API consumption into smaller independent contexts.\n* [**Store**](#store) - save and retrieve data in different scope levels (session, service and request).\n* [**Links**](#links-hateoas) - navigate through an API interacting with its links (HATEOAS for real).\n* [**Headers**](#headers) - directly create and parse complex headers.\n* [**URIs**](#uri) - build and parse complicated URIs easily.\n* [**Binary Data**](#binary-data) - upload and download files tracking the progress.\n* [**Form Data**](#form-data) - send both 'multipart/form-data' and 'application/x-www-form-urlencoded' requests.\n* [**Gzip Compression**](#gzip_encoding_enabled--boolean) - automatically encode and decode payloads with gzip.\n* [**Certificate Auth**](#certificate-authentication) - certificate based authentication made simple.\n\nRequestor is developed on top of three main pillars: (1) **Interoperability**, (2) **Simplicity**, and (3)\n**Extensibility**. In that fashion, **requestor-core** is developed in vanilla Java what makes it compatible\nwith any Java based platform and transpilable to other languages. To provide a fully working implementation, Requestor\nimpls are required to implement the dispatching mechanism through the wire. Currently, there are two requestor impls\navailable: **requestor-javanet** for JVM/Android and **requestor-gwt** for GWT2.\n\n\n## Preview\n\n👨‍💻 Create a Java app that queries the public IP, prints it and exits:\n\n```java\nfinal Session session = Requestor.newSession();\n\nsession.get(\"https://httpbin.org/ip\", String.class)    // make a GET request and read the body as String\n        .onSuccess( ip -\u003e System.out.println(ip) )     // print the body if response was 2xx\n        .onFail( res -\u003e System.out.println(\"Response status was \" + res.getStatus()) ) // print failure message if response was not 2xx\n        .onError( e -\u003e System.out.println(\"Request error: \" + e.getMessage()) );       // print error message if no response was received\n```\n\n🔥 In **Kotlin**:\n\n```kotlin\nrunBlocking {\n    val runner = CoroutineAsyncRunner(this)\n    val session = Requestor.newSession(runner)\n\n    session.get(\"https://httpbin.org/ip\", String::class.java)\n            .onSuccess { ip -\u003e println(ip) }\n            .onFail { _ -\u003e println(\"Unsuccessful response received\") }\n            .onError { _ -\u003e println(\"An error occurred during the request\") }\n}\n```\n\nCheck [here](#kotlin-coroutines) how to install **requestor-kotlin** extension and enable `CoroutineAsyncRunner` usage.\n\n🤔 Prefer sync programming?\n\n```java\nfinal Session session = Requestor.newSession();\n\ntry {\n    Response response = session.get(\"https://httpbin.org/ip\", String.class)\n            .await();\n\n    if (response.getStatus() == Status.OK) {\n        String ip = response.getPayload();\n        System.out.println(ip);\n    } else {\n        System.out.println(\"Unsuccessful response received\");\n    }\n} catch (RequestException e) {\n    System.out.println(\"An error occurred during the request\");\n    e.printStackTrace();\n}\n```\n\n👨‍💻 Make a POST request auto serializing an object into the request payload:\n\n```java\nBook book = new Book(\"Clean Code\", \"Robert C. Martin\", new Date(1217552400000L));\n\nsession.post(\"/api/books\", book)\n        .onSuccess( view::showSuccessMsg )\n        .onFail( view::showErrorMsg );\n```\n\n👨‍💻 Make a GET request auto deserializing the response payload to a collection of objects:\n\n```java\nsession.get(\"/api/books\", List.class, Book.class)\n        .onSuccess( books -\u003e render(books) );\n```\n\n**NOTE:** Check the [Serialization](#serialization) section to enable ***auto-serialization***.\n\nThe above examples are shortcuts in Session class to make quick requests.\nFurther, you can access the fluent API to build and send more complex requests as follows.\n\n### ✍️ Requesting Fluent API *(briefing)*\n\nRequesting involves three steps:\n1) Access the request builder by calling `session.req( \u003curi\u003e )`, and **set the request options** \n   through the chaining interface.\n2) Following, we must **call one of the invoke methods**, represented by the corresponding HTTP \n   methods (*get*, *post*, *put*, and so on). In this action, we specify the type we expect to \n   receive in the response payload.\n3) Finally, we receive a Request instance, which allows us to **chain callbacks** according to \n   different outcomes.\n\n\n```java\nsession.req(\"/api/books/1\")              // 0. Start building the request\n       .timeout(10_000)                  // 1. Set the request options\n       .header(\"ETag\", \"33a64df5\") \n       .get(Book.class)                  // 2. Invoke an HTTP method with the expected type\n       .onSuccess(book -\u003e render(book))  // 3. Add callbacks to the request\n       .onFail(response -\u003e log(response));\n```\n\nSee the [Requesting Fluent API](#requesting-fluent-api) section to know more details of how it \nworks.\n\nMeet all the request options available in the [Request Options](#request-options) section.\n\n### 🛠️ Set up your Session\n\nRequestor features a configurable client [Session](#session). There we *set default request options* \nthat apply to all requests. Additionally, since the `Session` is also a [Store](#store), we can use it to *save and\nretrieve data*, sharing state among components that rely on that session if necessary. \nEventually, we can *reset the session state* at any time.\n\n```java\nSession session = Requestor.newSession();\n\n// Set all requests to have 10s timeout and 'application/json' Content-Type\nsession.setTimeout(10_000);\nsession.setContentType(\"application/json\");\n\n// Perform login, save user info, and authenticate all subsequent requests\nsession.post(\"/login\", credentials, UserInfo.class)\n        .onSuccess(userInfo -\u003e {\n            // Save the user info in the session store\n            session.save(\"userInfo\", userInfo);\n            // Set the default auth for every session request\n            session.setAuth(new BearerAuth(userInfo.getToken()));\n        })\n        .await();\n\n// Make authenticated requests\nsession.post(\"/api/books\", book).await();\n\n// Retrieve data from the session store\nUserInfo userInfo = session.getValue(\"userInfo\");\n\n// Reset the session configuration to the defaults\nsession.reset();\n\n// Clear all data from the session store\nsession.clear();\n\n// Now all requests will have the default parameters\nsession.post(\"/api/books\", book);\n\n// Shutdown the session closing all underlying resources\nsession.shutdown();\n```\n\n\n### Looking for some REST? 😌\n\nRequestor offers a pre-defined REST client so that we can perform basic CRUD operations against \na resource. See the example below on how to create a new `RestService`.\n\n```java\nbookService = RestService\n        .of(Book.class, Integer.class) // \u003centityClass\u003e, \u003cidClass\u003e\n        .at(\"/api/books\")              // \u003crootPath\u003e\n        .on(session);                  // \u003csession\u003e\n\n// Configure your service to always set Content-Type and Accept headers as 'application/json'\nbookService.setMediaType(\"application/json\");\n\n// POST a book to '/api/books' and receive the created book from server\nBook book = new Book(\"RESTful Web Services\", \"Leonard Richardson\", new Date(1179795600000L));\nbookService.post(book).onSuccess(createdBook -\u003e render(createdBook));\n\n// GET all books from '/api/books'\nbookService.get().onSuccess(books -\u003e render(books));\n\n// GET books from '/api/books?author=Richardson\u0026year=2006'\nbookService.get(\"author\", \"Richardson\", \"year\", 2006).onSuccess(books -\u003e render(books));\n\n// GET the book of ID 123 from '/api/books/123'\nbookService.get(123).onSuccess(books -\u003e render(books));\n\n// PUT a book in the resource with ID 123 from '/api/books/123' and receive the updated book\nbookService.put(123, book).onSuccess(updatedBook -\u003e render(updatedBook));\n\n// PATCH book's title and year in '/api/books/123' and receive the updated book\nbookService.patch(123, book, \"title\", \"year\").onSuccess(updatedBook -\u003e render(updatedBook));\n\n// DELETE the book of ID 123 from '/api/books/123' (returns void)\nbookService.delete(123).onSuccess(() -\u003e showSuccess(\"Book was deleted.\"));\n```\n\nℹ️ Although Requestor provides this generic REST client, extending the `BaseService` class and \nimplementing our service clients is more beneficial. BaseService affords the advantage of\nlittle coding while empowering complete control of the requesting logic. Consequently, it \nimproves the testing capabilities and bug tracking. See more details in the [Service](#services) \nsection.\n\n\n## Installation\n\nRequestor primarily focuses on the HTTP Client API. Hence, **requestor-core** provides most of the\nfeatures but delegates some internals, like the network operation, to the implementations.\nThe ***requestor impls*** make the bridge between requestor-core and the target platform\n(JVM, Android, Browser, etc).\n\nCurrently, there are two requestor impls available:\n- **requestor-javanet** - it implements requestor for the **JVM** and **Android** platforms providing the network operation powered by the `java.net` package. \n- **requestor-gwt**. It implements requestor for **GWT2** apps that runs on the **Browser** providing the network operation powered by the `XMLHttpRequest`.\n\n### JVM / Android (Java / Kotlin)\n\nThe **requestor-javanet** impl is built with jdk8 and compatible with **Java 8+**.\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.reinert.requestor.impl\u003c/groupId\u003e\n    \u003cartifactId\u003erequestor-javanet\u003c/artifactId\u003e\n    \u003cversion\u003e1.4.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nIf you're using jdk12+ and want to make PATCH requests then add the following command line arg to execute your java app:\n`--add-opens java.base/java.net=ALL-UNNAMED`.\n\n### Kotlin Coroutines\n\nIf we need to integrate Requestor with Kotlin structured concurrency then we can install `requestor-kotlin` ext and use\n`CoroutineAsyncRunner` to start a `Session`.\n\nAdd `requestor-kotlin` dependency to the project:\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.reinert.requestor.ext\u003c/groupId\u003e\n    \u003cartifactId\u003erequestor-kotlin\u003c/artifactId\u003e\n    \u003cversion\u003e1.4.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nUse `CoroutineAsyncRunner` to create requestor sessions:\n\n```kotlin\n// inside a coroutine scope create the CoroutineAsyncRunner\nrunBlocking {\n    // we can optionally set the Dispatcher as the second arg\n    val runner = CoroutineAsyncRunner(this, Dispatchers.IO)\n    // create a requestor session using this AsyncRunner\n    val session = Requestor.newSession(runner)\n\n    // now every request is bound to this coroutine scope\n    session.req(\"https://httpbin.org/ip\")\n            .get(String::class.java)\n            .onSuccess { ip -\u003e println(ip) }\n\n    // await also works as expected\n    val req = session.get(\"https://httpbin.org/get\", String::class.java).await()\n    // this line is only executed after the request is finished because we called await\n    val ip: String = req.getPayload()\n    println(ip)\n}\n```\n\n### GWT2\n\nThe **requestor-gwt** impl is compatible with **GWT 2.7+** (Java 7+).\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.reinert.requestor.impl\u003c/groupId\u003e\n    \u003cartifactId\u003erequestor-gwt\u003c/artifactId\u003e\n    \u003cversion\u003e1.4.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nThen, make requestor available to your GWT project by importing the implementation's module.\n\n```xml\n\u003cinherits name=\"io.reinert.requestor.gwt.RequestorGwt\"/\u003e\n```\n\n### J2CL (GWT3)\n\nThis requestor impl is specified and we would love to have your contribution to help implementing it.\nIf you would like to get involded and make Requestor better, get in touch in our [community chat](#resources).\n\n### Latest Release\n\n1.4.0 (2 May 2024)\n\n\n## Request Options\n\nRequestor's Fluent API exposes many chaining methods to properly configure your request.\n\nSuppose we start building the request below:\n\n```java\nRequestInvoker req = session.req(\"/api/books/\");\n```\n\nWe then have the following options to set up our request.\n\n### *payload*\n\nSet an object as the request payload. This object is then serialized into the HTTP message's body\nas part of the [request processing](#processors-middlewares) after its invocation.\n\n```java\nreq.payload( \"a simple string\" );\nreq.payload( new Book(\"RESTful Web Services\", \"Leonard Richardson\", new Date(1179795600000L) );\nreq.payload( Arrays.asList(book1, book2, book3) );\n```\n\nOptionally, we can specify the fields we want to be serialized. In this case, the Serializer\nmust implement the required logic to filter the informed fields during serialization.\n\n```java\nBook book = new Book(\"RESTful Web Services\", \"Leonard Richardson\", new Date(1179795600000L);\nreq.payload( book, \"title\", \"author\" ); // Inform the serializer to filter title and author only\n```\n\n### *auth*\n\nSet an `Auth` implementation to properly authenticate your request. This async functional interface\nis capable of supplying the request with the required credentials and later sending it.\n\n```java\nreq.auth( new BasicAuth(\"username\", \"password\") );\nreq.auth( new BearerAuth(\"a-secret-token\") );\nreq.auth( new MyOwnAuth(\"whatever\") );\n```\n\n### *accept*\n\nA shortcut to set the Accept header.\n\n```java\nreq.accept( \"application/json\" );\n```\n\n### *contentType*\n\nA shortcut to set the Content-Type header.\n\n```java\nreq.contentType( \"application/json\" );\n```\n\n### *header*\n\nSet an HTTP header informing a key-value pair or a [Header](#headers-api) type.\n\n```java\nreq.header( \"Accept-Language\", \"da, en-gb;q=0.8, en;q=0.7\" );\nreq.header( new QualityFactorHeader(\"Accept-Language\", \"da\", 1.0, \"en-gb\", 0.8, \"en\", 0.7) );\n```\n\n### *timeout*\n\nSet a period in milliseconds in which the request should be timeout.\n\n```java\nreq.timeout( 10_000 ); // Timeout after 10s\n```\n\n### *delay*\n\nSet a time in milliseconds to postpone the request sending.\nThe [request processing](#processors-middlewares) will happen only after the delay period. \n\n```java\nreq.delay( 5_000 ); // Delay the request for 5s\n```\n\n### *skip*\n\nSkip the request/response [processors](#processors-middlewares).\n\nExample skipping the [authentication](#authentication):\n```java\nreq.skip( Process.AUTH_REQUEST );\n```\n\nExample skipping filters:\n```java\nreq.skip( Process.FILTER_REQUEST, Process.FILTER_RESPONSE );\n```\n\nExample skipping interceptors:\n```java\nreq.skip( Process.INTERCEPT_REQUEST, Process.INTERCEPT_RESPONSE );\n```\n\nExample skipping serializers:\n```java\nreq.skip( Process.SERIALIZE_REQUEST, Process.DESERIALIZE_RESPONSE );\n```\n\n### *retry*\n\nThere are two ways of defining a retry policy: one by informing the events that trigger a retry and a sequence of delays;\nand another by implementing a `RetryPolicy`.\n\n#### *retry( delays, events )*\n\nSet a retry policy for the request with two arguments: (1) an array of `delays` in milliseconds and (2) an array of `events`.\nThe **delays** array is a sequence of times that Requestor will wait before retrying the request respectively.\nIt also indicates the number of retries that will be performed at most. Furthermore, the **events** array is\na set of events that will trigger a retry. Occurring a request event that is defined in the retry police,\nthe [callbacks](#event-driven-callbacks) bound to those events are not executed and a retry is triggered. These callbacks will be executed only\nafter all retries defined in the retry police were made and the retry event persisted to occur.\n\nRegarding the `delays` argument, although we can provide an int array manually, we will often resort to the `DelaySequence`.\nIt is a factory that provides helpful methods to create sequences of delays according to different criteria.\n* **DelaySequence.arithmetic**( *\\\u003cinitialSeconds\\\u003e, \\\u003ccommonDiff\\\u003e, \\\u003climit\\\u003e* ) - creates a millis int array with an arithmetic sequence.\n  * Ex: `DelaySequence.arithmetic(5, 20, 3)` generates `[5s, 25s, 45s]` \n* **DelaySequence.geometric**( *\\\u003cinitialSeconds\\\u003e, \\\u003cratio\\\u003e, \\\u003climit\\\u003e* ) - creates a millis int array with a geometric sequence.\n  * Ex: `DelaySequence.geometric(3, 2, 4)` generates `[3s, 6s, 12s, 36s]`\n* **DelaySequence.fixed**( *\\\u003cseconds\\\u003e...* ) - creates a sequence with the given seconds array multiplied by 1000.\n  * Ex: `DelaySequence.fixed(5, 15, 45)` generates `[5s, 15s, 45s]`\n\nAs for the `events` argument, Requestor has a set of pre-defined events in the `RequestEvent` enum, matching the events\nthat we can bind [callbacks](#event-driven-callbacks) to. Additionally, any `StatusFamily` or `Status` is also an event.\nCheck some examples of events:\n* `RequestEvent.FAIL` - request receives a response with status ≠ 2xx\n* `RequestEvent.TIMEOUT` - request has timed out with no response\n* `RequestEvent.CANCEL` - request has been cancelled before receiving a response\n* `RequestEvent.ABORT` - request has been aborted before sending either manually or due to a runtime exception in the processing cycle\n* `StatusFamily.CLIENT_ERROR` - request receives a response with status = 4xx\n* `StatusFamily.SERVER_ERROR` - request receives a response with status = 5xx\n* `Status.TOO_MANY_REQUESTS` - request receives a response with status = 429\n* `Status.of(429)` - request receives a response with status = 429\n\n```java\n// Set the request to retry on 'timeout' or '429' responses\nreq.retry( DelaySequence.geometric(3, 2, 4), RequestEvent.TIMEOUT, Status.TOO_MANY_REQUESTS )\n\n// Set the request to retry on 'cancel' or '429' and '503' responses\nreq.retry( DelaySequence.arithmetic(5, 20, 3), RequestEvent.CANCEL, Status.of(429), Status.of(503) )\n\n// Set the request to retry on 'timeout', '4xx' and '529'\nreq.retry( DelaySequence.fixed(5, 15, 45), RequestEvent.TIMEOUT, StatusFamily.CLIENT_ERROR, Status.SERVICE_UNAVAILABLE )\n```\n\n#### *retry( RetryPolicy )*\n\nFurthermore, we are able to provide a more complex retry logic by implementing the `RetryPolicy` functional interface.\n\nSee this example implementing an exponential backoff retry with random jitter:\n\n```java\npublic class ExponentialWithJitterRetryPolicy implements RetryPolicy {\n    \n    public int retryIn(RequestAttempt attempt) {\n        // first check the conditions that would make us stop retrying be returning -1\n\n        // event is the result of the request which can be a response or an exception\n        if (!StatusFamily.SERVER_ERROR.includes(attempt.getEvent())) {\n            // we should retry only 5xx responses\n            return -1;\n        }\n\n        // retryCount holds the number of times this request was already retried\n        if (attempt.getRetryCount() \u003e 2) {\n            // we should retry at most three times\n            return -1;\n        }\n        \n        // exponentially increase the next timeouts by a ratio of 2\n        attempt.setTimeout(attempt.getTimeout() * 2);\n\n        // next retry will happen in two to the power of the number of past retries\n        int expDelay = (int) (Math.pow(2, attempt.getRetryCount()) * 1000);\n\n        // let's add a random jitter to increase the chance of avoiding collisions\n        int jitter = new Random().nextInt(1000);\n\n        // return the time in milliseconds that the next retry should happen\n        return expDelay + jitter;\n    }\n}\n```\n\nCheck out an example implementing a Retry-After header retry police:\n\n```java\npublic class RetryAfterHeaderRetryPolicy implements RetryPolicy {\n    \n    public int retryIn(RequestAttempt attempt) {\n        // the request can fail due to a non success Response or a RequestException\n\n        if (attempt.isResponseAvailable()) {\n            // if the request failed due to a non success Response\n            // then check if the Retry-After exists\n            Response response = attempt.getResponse();\n            \n            if (response.hasHeader(\"Retry-After\")) {\n                // get the value of the Retry-After header\n                String seconds = response.getHeader(\"Retry-After\");\n                \n                // return the Retry-After value converted to milliseconds\n                return Integer.parseInt(seconds) * 1000;\n            }\n        }\n\n        // if the Retry-After header is not present then don't retry\n        return -1;\n    }\n}\n```\n\nOnce we have implemented our custom `RetryPolicy`, we can set it in the request either with an instance or a Provider.\n\n```java\n// register a new instance of our custom retry policy\nreq.retry( new ExponentialWithJitterRetryPolicy() );\n```\n\nWhen our customized retry policy contains state that should not be shared among different requests,\nwe must register it with a Provider instead.\n\n```java\n// register a Provider of our custom retry policy\nreq.retry( ExponentialWithJitterRetryPolicy::new );\n```\n\nBy setting a Provider instead of an instance, every new request will have a different instance of the RetryPolicy.\n\n### *poll*\n\nNeeding to ping an endpoint in a specific interval, we can set the poll option with the \n`PollingStrategy` and the `interval` in milliseconds. Additionally, we can set the maximum \nnumber of requests to stop the polling by informing the `limit` argument.\n\n```java\n// Send the request each 3s\nreq.poll( PollingStrategy.SHORT, 3_000 );\n\n// Send the request each 3s up to the limit of 10 requests\nreq.poll( PollingStrategy.SHORT, 3_000, 10 );\n```\n\nThere are two PollingStrategy choices: **LONG** or **SHORT**.\n* `PollingStrategy.LONG` - Long polling means the subsequent request will be dispatched only \n  **after receiving the previous response**.\n  * If we set an interval, the following request will only be delayed after the previous \n    response has come.\n* `PollingStrategy.SHORT` - On the other hand, with Short polling, the subsequent request will \n  be dispatched right **after the previous request has been sent**.\n    * If we set an interval, then the following request will be delayed by this interval as \n      soon the previous request is dispatched.\n\n```java\n// The next requests are dispatched 3s after the previous ones\nreq.poll( PollingStrategy.SHORT, 3_000 );\n\n// The next requests are dispatched as soon the responses are received\nreq.poll( PollingStrategy.LONG ); // Same as `req.poll( PollingStrategy.LONG, 0 )`\n\n// The next requests are dispatched 10s after previous responses up to the limit of 5 requests\nreq.poll( PollingStrategy.LONG, 10_000, 5 );\n```\n\nIn both cases, if we also set the request's delay option, then the subsequent dispatches' \n*total delay* = *request delay* + *polling interval*.\n\n```java\n// The first request is delayed by 2s and the next ones are delayed by 5s (2 + 3)\nreq.delay(2_000).poll( PollingStrategy.SHORT, 3_000 );\n```\n\nFurthermore, not setting a *polling limit*, we can manually ***stop*** the polling by calling \n`request.stopPolling()`.\n\n```java\nsession.req(\"/api/books/\")\n       .poll(PollingStrategy.LONG)\n       .get()\n       .onLoad((response, request) -\u003e {\n           if (request.getPollingCount() == 3) {\n               request.stopPolling(); // Stop polling after receiving the third response\n           }\n       });\n```\n\nIt is worth noting that each new dispatched request will pass through all the [request/response \nprocessing cycle](#processors-middlewares). Thereby, we will have every polling request always up \nto date with our filters, serializers, and interceptors.\n\nFinally, if we set a retry police, each dispatched request will execute the retries individually as well,\nbefore triggering the retry events.\n\n#### 🔥 Polling in sync flow using *await*\n\nWe can poll a request in a sync flow by leveraging the [*await*](#await) feature.\nCheck how it looks like:\n\n```java\n// Create a polling request\nPollingRequest\u003cString\u003e req = session.req(\"https://httpbin.org/get\")\n        .poll(PollingStrategy.LONG)\n        .get(String.class);\n\n// While the request is polling, await for each response and act\nwhile (req.isPolling()) {\n    try {\n        Response res = req.await();\n\n        doSomething(res);\n\n        if (shouldStop(req, res)) {\n            req.stopPolling();\n        }\n    } catch (RequestException e) {\n        handleError(e);\n    }\n}\n```\n\n## Event-Driven Callbacks\n\nRequestor defines an event system according to the [Request Lifecycle](#processors-middlewares).\nWe can add as many callbacks as we need for each event that may occur.\nThe events are divided into three main categories: **Load** events, **Error** events, and **Progress** events.\nThe `error` event is triggered whenever the request is interrupted before receiving a response.\nIt is subdivided into three events: `abort` (request interrupted before being sent), `cancel` (request interrupted after being sent), and `timeout` (request expired before receiving a response).\nThe `load` event is triggered whenever a request receives a response. It is subdivided into two events: `success` (2xx response received), `fail` (~2xx response received).\nAdditionally, any Status Code is also an event. So, if a 201 response is received, the `201` event is triggered.\nFinally, the Status Families are also events. Thus, a 201 response triggers the `2` event as well.\n\nBesides, Requestor also fires two **progress** events: the `read` and `write` events. They enable us to track download and upload progress respectively.\nWhile the `error` and `load` (and their children) events are triggered once per request call, the `read` and `write` events are triggered many times per call.\nThese events also allow us to get each chunk of bytes that is sent or received during a request. They open the [HTTP Streaming](#http-streaming) door for us.\n\n![request-events](https://user-images.githubusercontent.com/1285494/146399333-8294288f-b5b8-4cf6-bcee-e8e2fe939695.png)\n\nAfter invoking a request, we receive a `Request\u003cT\u003e` instance. It is a deferred object that \nallows us to chain callbacks to handle any event that the request may produce. Besides, \nwe can also access all request options that formed this request.\n\nOne `Request\u003cT\u003e` will fire either a `load` event or an `error` event only once.\nBut when setting the [poll](#poll) option while building a request, we receive a `PollingRequest\u003cT\u003e`.\nIt may trigger `error` or `load` events many times, once per each call that is made.\nAdditionally, the `PollingRequest\u003cT\u003e` interface extends from `Request\u003cT\u003e` and allows us to access the polling options that formed it as also manually stop the poll.\nNotice, nevertheless, that Requestor treats every request as a polling request under the hood.\nSo even a simple request is a polling request of one call only.\nThat is the reason why every request needs a [Deferred Pool Factory](#deferredpool-factory). \n\n### ☑️ Load events\n\n#### **.onSuccess**( [payload [, response [, request]]] -\u003e {} )\n* This callback is executed when the response *is successful* (status = 2xx)\n* It features the *deserialized payload*, the *response* and the *request* arguments\n* All arguments are optional\n\nExample setting a no-arg callback to the success event:\n```java\nsession.get(\"/health-check\")\n        .onSuccess(() -\u003e System.out.println(\"alive!\"));\n```\n\nExample setting a callback with the payload arg to the success event:\n```java\nsession.get(\"/profile\", UserProfile.class)\n        .onSuccess(profile -\u003e render(profile));\n```\n\nExample setting a callback with the payload and response args to the success event:\n```java\nsession.req(\"/profile\").get(UserProfile.class)\n        .onSuccess((profile, res) -\u003e render(profile, res));\n```\n\nExample setting a callback with the payload, response and request args to the success event:\n```java\nsession.get(\"/endpoint\")\n        .onSuccess((none, res, req) -\u003e render(res, req));\n```\n\n**NOTE:** All callbacks throw exceptions, so we are exempted from handling checked exceptions inside them.\nIf an exception occurs in a callback logic, the stack trace is printed out, and it does not affect other\ncallbacks that were added to the request.\n\n#### **.onFail**( [response [, request]] -\u003e {} )\n* This callback is executed when the response *is unsuccessful* (status ≠ 2xx)\n* It features the *response* and the *request* arguments\n* All arguments are optional\n\nExample setting a no-arg callback to the fail event:\n```java\nsession.get(\"/health-check\")\n        .onFail(() -\u003e System.out.println(\"dead!\"));\n```\n\nExample setting a callback with the response arg to the fail event:\n```java\n// Print the response body as string\nsession.get(\"/endpoint\")\n        .onFail(res -\u003e print(res.getSerializedPayload().asString()));\n```\n\nExample setting a callback with the response and request args to the fail event:\n```java\nsession.get(\"/endpoint\")\n        .onFail((res, req) -\u003e {\n            System.out.println(res.getStatusCode());\n            System.out.println(req.getUri());\n        });\n```\n\n#### **.onStatus**( statusCode | statusFamily, ( [response [, request]] ) -\u003e {} )\n* This callback is executed when the response returns the given *Status Code* or *Status Family*\n* It features the *response* and the *request* arguments\n* All arguments are optional\n\nExample setting a no-arg callback to the 204 status event:\n```java\nsession.get(\"/health-check\")\n        .onStatus(204, () -\u003e System.out.println(\"alive!\"));\n```\n\nExample setting callbacks with the response arg to the 404 and 500 status events:\n```java\nsession.get(\"/endpoint\")\n        .onStatus(Status.NOT_FOUND, res -\u003e handleNotFound(res))\n        .onStatus(Status.INTERNAL_SERVER_ERROR, res -\u003e handleServerError(res));\n```\n\nExample setting callbacks with the response and request args to the 2xx, 4xx and 5xx status family events:\n```java\nsession.req(\"/endpoint\").get()\n        .onStatus(StatusFamily.SUCCESSFUL, (res, req) -\u003e handleSuccess(res, req))\n        .onStatus(StatusFamily.CLIENT_ERROR, (res, req) -\u003e handleClientError(res, req))\n        .onStatus(StatusFamily.SERVER_ERROR, (res, req) -\u003e handleServerError(res, req));\n```\n\n#### **.onLoad**( [response [, request]] -\u003e {} )\n* This callback is executed when any response is returned\n* It features the *response* and the *request* arguments\n* All arguments are optional\n\nExample setting a callback with the response and request args to the load event:\n```java\nsession.get(\"/endpoint\")\n        .onLoad((res, req) -\u003e {\n            print(res.getStatusCode());\n            print(req.getUri());\n        });\n```\n\n### ⛔ Error events\n\n#### **.onAbort**( exception [, request] -\u003e {} )\n* This callback is executed if the request was *aborted before being sent* (either manually by the user or due to any runtime error)\n* It features the *exception* and the *request* arguments\n* The *request* argument is optional\n\nExample setting a callback with the exception and request args to the abort event:\n```java\nsession.get(\"/endpoint\")\n        .onAbort((exc, req) -\u003e {\n            print(exc.getMessage());\n            print(req.getUri());\n        });\n```\n\n#### **.onCancel**( exception [, request] -\u003e {} )\n* This callback is executed if the request was *cancelled after being sent* (either manually by the user or due to network error)\n* It features the *exception* and the *request* arguments\n* The *request* argument is optional\n\nExample setting a callback with the exception arg to the cancel event:\n```java\nsession.get(\"/endpoint\")\n        .onCancel(exc -\u003e print(exc.getMessage()));\n```\n\n#### **.onTimeout**( exception [, request] -\u003e {} )\n* This callback is executed a timeout occurs\n* It features the *exception* and the *request* arguments\n* The *request* argument is optional\n\nExample setting a callback with the exception arg to the timeout event:\n```java\nsession.get(\"/endpoint\")\n        .onTimeout(exc -\u003e {\n            print(exc.getTimeoutMillis());\n            print(exc.getUri());\n        });\n```\n\n#### **.onError**( [exception [, request]] -\u003e {} )\n* This callback is executed if the request was *aborted before being sent* (either manually by the user or due to any runtime error)\n* It features the *exception* and the *request* arguments\n* All arguments are optional\n\nExample setting a callback with the exception and request args to the abort event:\n```java\nsession.get(\"/endpoint\")\n        .onAbort((exc, req) -\u003e {\n            print(exc.getCause());\n            print(req.getMethod());\n        });\n```\n\n**NOTE:** If an error occurs, and no callback for that error kind was set, than the exception stack trace is printed.\nThus, the errors are not hidden if we forgot to add callbacks for them.\n\n### 🚧 Progress events\n\n#### **.onRead**( progress -\u003e {} )\n* This callback is executed each time a chuck of bytes is *received*\n* It features the *read progress* argument with:\n  * the request\n  * the incoming response\n  * the loaded and total byte length\n  * the read byte chunk\n\nExample setting a callback with the read progress arg to the read event:\n```java\n// Enable read chunking (a.k.a. streaming) on the session\nsession.save(Requestor.READ_CHUNKING_ENABLED, true);\n\nsession.get(\"/endpoint\").onRead(progress -\u003e {\n    if (progress.isLengthComputable()){\n        print(progress.getTotal());\n        print(progress.getLoaded());\n        // print loaded / total * 100\n        print(progress.getCompletedFraction(100));\n    }\n\n    // check if read chunking is enabled\n    if (progress.isChunkAvailable()) {\n        print(progress.getChunk().asBytes());\n        print(progress.getChunk().asString());\n    }\n\n    print(progress.getRequest().getUri());\n    print(progress.getResponse().getHeaders());\n});\n```\n\n#### **.onWrite**( progress -\u003e {} )\n* This callback is executed each time a chuck of bytes is *sent*\n* It features the *write progress* argument with:\n    * the request\n    * the loaded and total byte length\n    * the written byte chunk\n\nExample setting a callback with the write progress arg to the write event:\n```java\nFile file = getFile();\n\nsession.req(\"/endpoint\")\n        // Enable write chunking (a.k.a. streaming) on the request\n        .save(Requestor.WRITE_CHUNKING_ENABLED, true)\n        .payload(file)\n        .post()\n        .onWrite(progress -\u003e {\n            if (progress.isLengthComputable()){\n                print(progress.getTotal());\n                print(progress.getLoaded());\n                // print loaded / total * 100\n                print(progress.getCompletedFraction(100));\n            }\n        \n            // check if write chunking is enabled\n            if (progress.isChunkAvailable()) {\n                print(progress.getChunk().asBytes());\n                print(progress.getChunk().asString());\n            }\n        \n            print(progress.getRequest().getUri());\n        });\n```\n\n### Success callbacks and Collections\n\nWhen requesting, we will always receive a `Request\u003cCollection\u003cT\u003e\u003e` despite the particular \ncollection type (*List*, *Set*, and so on) we asked due to a design limitation of the Java \nlanguage, which does not allow \"generics of generics.\" So, if we need the particular type we\nasked, we need to explicitly typecast the payload.\nSee the example:\n\n```java\n// An ArrayList was requested, but the get method returned a Request\u003cCollection\u003cBook\u003e\u003e\nRequest\u003cCollection\u003cBook\u003e\u003e request = session.req(\"/server/books\").get(ArrayList.class, Book.class);\n\n// If wee need to access the List interface, we must typecast the paylaod\nrequest.onSuccess(books -\u003e {\n    List\u003cBook\u003e bookList = (List\u003cBook\u003e) books;\n    Book firstBook = bookList.get(0);\n});\n```\n\n\n## Futures\n\n\u003csup\u003e\u003cb\u003e@GwtIncompatible\u003c/b\u003e\u003c/sup\u003e\n\nBesides providing the async interface to handle requests results, Requestor exposes some Futures\nin the three main milestones of the Request-Response lifecycle.\n\n### 1. Request.getResponse() : Future\\\u003cIncomingResponse\\\u003e\n\nThe first milestone occurs when the response header is received but the body is still going to be read.\n\nAs soon the response header is available we receive it through the `getResponse()` method of the `Request`.\n\n```java\nFuture\u003cIncomingResponse\u003e future = session.req(\"https://httpbin.org/ip\")\n        .get(String.class) // the type to later deserialize the response body\n        .getResponse(); // get the response future\n\n// Calling future.get() holds until the response header is received\nIncomingResponse response = future.get();\n```\n\nThe `IncomingResponse` provides us access to the response **status**, **headers**, **links** and the request\n**store** among other useful things.\n\n### 2. IncomingResponse.getSerializedPayload() : Future\\\u003cSerializedPayload\\\u003e\n\nThe second milestone occurs when the response body is read but is still going to be processed and deserialized.\n\nAs soon the response body is available we receive it through the `getSerializedPayload()` method of the `IncomingResponse`.\n\n```java\nFuture\u003cSerializedPayload\u003e future = response.getSerializedPayload();\n\n// Calling future.get() holds until the response body is received\nSerializedPayload serializedPayload = future.get();\n```\n\nThe `SerializedPayload` provides us access to the raw response **body** as bytes or string.\n\n### 3. IncomingResponse.getPayload() : Future\\\u003cT\\\u003e\n\nThe third milestone occurs when the response passes through all [processors](#processors-middlewares)\nand is finally made available to the caller.\n\nAfter the response is completely processed, its body is already deserialized, and we receive it\nthrough the `getPayload()` method of the `IncomingResponse`. This method automatically typecasts\nto the desired type.\n\n```java\n// Automatically typecasts to the desired type\nFuture\u003cString\u003e future = response.getPayload();\n\n// Calling future.get() holds until the response finishes processing\nString ip = future.get();\n```\n\nCheck a complete example of how to use Requestor's Future API.\n\n```java\nfinal Session session = Requestor.newSession();\n\ntry {\n    // Make a request and get the response future\n    Future\u003cIncomingResponse\u003e responseFuture =\n            session.get(\"https://httpbin.org/ip\", String.class)\n                    .getResponse();\n\n    // As soon the response header is received we get it\n    IncomingResponse response = responseFuture.get();\n\n    // Although the response was not completely received\n    // we can promptly access its status, headers and store\n    if (response.getStatus() == Status.OK) {\n        // As soon the response body is read we get it\n        Future\u003cSerializedPayload\u003e serializedPayloadFuture = response.getSerializedPayload();\n        SerializedPayload serializedPayload = serializedPayloadFuture.get();\n\n        // The raw payload is yet going to be deserialized\n        // but we can already access its content as bytes or string\n        System.out.println(serializedPayload.asString());\n\n        // When the response is completely received it is submitted to the processors\n        // After being processed the deserialized payload is made available\n        Future\u003cString\u003e payloadFuture = response.getPayload();\n        String ip = payloadFuture.get();\n        System.out.println(ip);\n    } else {\n        System.out.println(\"Unsuccessful response received\");\n    }\n} catch (InterruptedException | ExecutionException e) {\n    System.out.println(\"An error occurred during the request\");\n    e.printStackTrace();\n}\n\n// Close all threads and finish the program\nsession.shutdown();\n```\n\n\n## Await\n\n\u003csup\u003e\u003cb\u003e@GwtIncompatible\u003c/b\u003e\u003c/sup\u003e\n\nRequestor exposes a way to wait for the request to finish thus allowing the user to  code in a synchronous fashion.\n\nBy calling `Request.await()` we instruct the current thread to hold until the `Response` is available and returned.\n\n```java\nfinal Session session = Requestor.newSession();\n\ntry {\n    Response response = session.req(\"https://httpbin.org/ip\")\n            .get(String.class)\n            .await();\n\n    if (response.getStatus() == Status.OK) {\n        String ip = response.getPayload();\n        System.out.println(ip);\n    } else {\n        System.out.println(\"Unsuccessful response received\");\n    }\n} catch (RequestException e) {\n    System.out.println(\"An error occurred during the request\");\n    e.printStackTrace();\n}\n```\n\n\n## Serialization\n\nSerialization is part of the [Request Processing](#processors-middlewares), and deserialization is \npart of the [Response Processing](#processors-middlewares).\n\nRequestor exposes the `Serializer` interface responsible for serializing and deserializing a \nspecific type while holding the **Media Types** it handles. Therefore, it is possible \nto have multiple Serializers for the same Java Type handling different Media Types, e.g., JSON and \nXML. Requestor's serialization engine is smart enough to **match** the appropriate **Serializer** \naccording to the asked class and the request/response's **Content-Type**.\n\nTo enable serialization and deserialization, we must register a `Serializer` instance in the\n[session](#session). Necessitating only the deserialization part, we can register a \n`Deserializer` implementation, since the Serializer extends from the Deserializer.\n\n```java\nsession.register(new MyTypeSerializer());\n```\n\nAdditionally, we can register a `Provider` instead of an actual Serializer instance. It is a \nfactory that will provide the Serializer instance on demand. When Requestor's serialization engine \nmatches a serializer according to the class and media type, it asks the Provider for an \ninstance. If this instance is disposable (i.e., it is not living in an outer context), the \ngarbage collector will free up some memory after its usage. See how to register a Serializer \nProvider:\n\n```java\nregister(new SerializerProvider() {\n    public Serializer\u003c?\u003e getInstance() {\n        return new MyTypeSerializer(); // return a disposable instance instead of a reference\n    }\n});\n\n// Lambda syntax\nsession.register(MyTypeSerializer::new); // Same as `session.register(() -\u003e new MyTypeSerializer())`\n```\n\n**💡 PRO TIP**: If you start having too many serializers, consider registering them with `Providers` to save memory.\n\nAlthough it is possible to implement our custom Serializers, we often resort to **AUTO-SERIALIZATION**\nprovided by requestor extensions. The existing auto-serialization exts are listed following.\n\n\n### Gson auto-serialization (JVM/Android)\n\nThe `requestor-gson` extension integrates [Gson](https://github.com/google/gson) with Requestor\nto enable auto serialization.\n\nFirst, register the `GsonSerializer` in the Session, then start requesting:\n\n```java\nsession.register(new GsonSerializer());\n\nsession.post(\"/books\", book);\n\nsession.get(\"/books\", List.class, Book.class);\n\nsession.get(\"/booksById\", Map.class, Long.class, Book.class);\n```\n\nWe can define a custom `Gson` instance when instantiating the `GsonSerializer`:\n\n```java\nGson prettyGson = new GsonBuilder().setPrettyPrinting().create();\nsession.register(new GsonSerializer(prettyGson));\n```\n\nIn order to install requestor-gson extension, add the following dependency to your project:\n\n```xml\n\u003cdependencies\u003e\n  \u003cdependency\u003e\n    \u003cgroupId\u003ecom.google.code.gson\u003c/groupId\u003e\n    \u003cartifactId\u003egson\u003c/artifactId\u003e\n    \u003cversion\u003e${gson.version}\u003c/version\u003e\n  \u003c/dependency\u003e\n  \u003cdependency\u003e\n    \u003cgroupId\u003eio.reinert.requestor.ext\u003c/groupId\u003e\n    \u003cartifactId\u003erequestor-gson\u003c/artifactId\u003e\n    \u003cversion\u003e${requestor.version}\u003c/version\u003e\n  \u003cdependency\u003e\n\u003c/dependencies\u003e\n```\n\n\n### Gwt-Jackson auto-serialization (GWT)\n\nThe `requestor-gwtjackson` extension integrates gwt-jackson into requestor to provide auto \nserialization. Intending to have the Serializers automatically generated, we just need to \ndeclare a `SerializationModule` annotating it with the required classes. Additionally, we can \nset the **Media Types** the serializers should handle. Since gwt-jackson serializes to JSON \nformat, \"application/json\" is the default media type, but you can declare other media types, even \nwildcard types like \"\\*/json\" or \"\\*/\\*\".\n\n```java\n@MediaType({\"application/*json*\", \"*/javascript\"})\n@JsonSerializationModule({ Author.class, Book.class })\ninterface MySerializationModule extends SerializationModule {}\n```\n\nAccording to this example, when asking for `Author.class` or `Book.class`, all three scenarios \nbelow will match the auto-generated serializers:\n\n```java\n// Matches 'application/*json*' to serialize the payload\nsession.req(\"/books\").contentType(\"application/json+gzip\").payload(book).post();\n\n// Matches 'application/*json*' to deserialize the response's payload\n// assuming the response's content-type was really 'application/stream+json'\nsession.req(\"/books\").accept(\"application/stream+json\").get(List.class, Book.class);\n\n// Matches '*/javascript' to deserialize the response's payload\n// assuming the response's content-type was really 'text/javascript'\nsession.req(\"/authors/1\").accept(\"text/javascript\").get(Author.class);\n```\n\nWe do not need to register the auto-generated serializers in our session instance. This \nis automatically done under the hood if we instantiate a `JsonSession`.\n\n```java\n// All generated serializers will be automatically registered in this session\nSession session = RequestorGwtJackson.newSession();\n```\n\nIn order to install requestor-gwtjackson extension, add the following dependency to your project:\n\n```xml\n\u003cdependencies\u003e\n  \u003cdependency\u003e\n    \u003cgroupId\u003ecom.github.nmorel.gwtjackson\u003c/groupId\u003e\n    \u003cartifactId\u003egwt-jackson\u003c/artifactId\u003e\n    \u003cversion\u003e${gwtjackson.version}\u003c/version\u003e\n  \u003c/dependency\u003e\n  \u003cdependency\u003e\n    \u003cgroupId\u003eio.reinert.requestor.ext\u003c/groupId\u003e\n    \u003cartifactId\u003erequestor-gwtjackson\u003c/artifactId\u003e\n    \u003cversion\u003e${requestor.version}\u003c/version\u003e\n  \u003cdependency\u003e\n\u003c/dependencies\u003e\n```\n\nThen inherit the `RequestorGwtJackson` GWT module in your gwt.xml file:\n\n```xml\n\u003cinherits name=\"io.reinert.requestor.gwtjackson.RequestorGwtJackson\"/\u003e\n```\n\n### AutoBean auto-serialization (GWT)\n\nSimilarly to the previous extension, `requestor-autobean` provides auto serialization for \nAutoBean interfaces. Likewise, we declare a SerializationModule with the classes and media types \nto generate the serializers.\n\n```java\n@MediaType({\"application/json\", \"*/*\"})\n@AutoBeanSerializationModule({ Author.class, Book.class })\ninterface MySerializationModule extends SerializationModule {}\n```\n\nWe do not need to register the auto-generated serializers in our session instance. This\nis automatically done under the hood if we instantiate an `AutoBeanSession`.\n\n```java\n// All generated serializers will be automatically registered in this session\nSession session = RequestorAutoBean.newSession();\n```\n\nFurther, Requestor graciously enables us to create new AutoBean instances directly from the \nSession by calling `session.getInstance(\u003cClass\u003e)`.\n\n```java\nBook book = session.getInstance(Book.class);\n```\n\nThe installation procedure is conventional.\n\n```xml\n\u003cdependencies\u003e\n  \u003cdependency\u003e\n    \u003cgroupId\u003eio.reinert.requestor.ext\u003c/groupId\u003e\n    \u003cartifactId\u003erequestor-autobean\u003c/artifactId\u003e\n    \u003cversion\u003e${requestor.version}\u003c/version\u003e\n  \u003cdependency\u003e\n\u003c/dependencies\u003e\n```\n\nThen inherit the `RequestorAutoBean` GWT module in your gwt.xml file:\n\n```xml\n\u003cinherits name=\"io.reinert.requestor.autobean.RequestorAutoBean\"/\u003e\n```\n\n### Implementing a Serializer\n\nBesides auto-serialization, Requestor's users can also write their own custom serializers. Check \nthe following sample of a raw handwritten `Serializer`.\n\n```java\nclass BookXmlSerializer implements Serializer\u003cBook\u003e {\n\n    @Override\n    public Class\u003cBook\u003e handledType() {\n        return Book.class;\n    }\n\n    @Override\n    public String[] mediaType() {\n        // Return an array of media-type patterns; wildcards are accepted\n        return new String[]{ \"*/xml*\" };\n    }\n\n    @Override\n    public SerializedPayload serialize(Book book, SerializationContext ctx) {\n        return new TextSerializedPayload(\n                \"\u003cbook\u003e\u003ctitle\u003e\" + book.getTitle() + \"\u003c/title\u003e\"\n                + \"\u003cauthor\u003e\" + book.getAuthor() + \"\u003c/author\u003e\"\n                + \"\u003cpubDate\u003e\" + book.getPubDate().getTime() + \"\u003c/pubDate\u003e\u003c/book\u003e\");\n    }\n\n    @Override\n    public SerializedPayload serialize(Collection\u003cBook\u003e books, SerializationContext ctx) {\n        StringBuilder sb = new StringBuilder(\"\u003carray\u003e\");\n        for (Book b : books) sb.append(serialize(b, ctx).asString());\n        sb.append(\"\u003c/array\u003e\");\n        return new TextSerializedPayload(sb.toString());\n    }\n\n    @Override\n    public Book deserialize(SerializedPayload payload, DeserializationContext ctx) {\n        String response = payload.asString();\n        int titleStart = response.indexOf(\"\u003ctitle\u003e\") + 7;\n        int titleEnd = response.indexOf(\"\u003c/title\u003e\", titleStart);\n        String title = response.substring(titleStart, titleEnd);\n\n        int authorStart = response.indexOf(\"\u003cauthor\u003e\", titleEnd) + 8;\n        int authorEnd = response.indexOf(\"\u003c/author\u003e\", authorStart);\n        int author = Integer.parseInt(response.substring(authorStart, authorEnd));\n\n        int pubDateStart = response.indexOf(\"\u003cpubDate\u003e\", authorEnd) + 9;\n        int pubDateEnd = response.indexOf(\"\u003c/pubDate\u003e\", pubDateStart);\n        Date pubDate = new Date(Long.parseLong(response.substring(pubDateStart, pubDateEnd)));\n\n        return new Book(title, author, pubDate);\n    }\n\n    @Override\n    public \u003cC extends Collection\u003cBook\u003e\u003e C deserialize(Class\u003cC\u003e collectionType, SerializedPayload payload, DeserializationContext ctx) {\n        String response = payload.asString();\n        C collection = ctx.getInstance(collectionType);\n\n        int cursor = response.indexOf(\"\u003cbook\u003e\");\n        while (cursor != -1) {\n            int cursorEnd = response.indexOf(\"\u003c/book\u003e\", cursor);\n            collection.add(deserialize(new TextSerializedPayload(response.substring(cursor + 6, cursorEnd)), ctx));\n            cursor = response.indexOf(\"\u003cbook\u003e\", cursorEnd);\n        }\n\n        return collection;\n    }\n}\n```\n\n#### Inheritance\n\nAdditionally, when the `Serializer` should handle sub-types of the target type, we can extend \nthe `HandlesSubTypes\u003cT\u003e` interface and implement the `handledSubTypes` method, like below: \n\n\n```java\n// Implement the HandlesSubTypes interface\nclass BookXmlSerializer implements Serializer\u003cBook\u003e, HandlesSubTypes\u003cBook\u003e {\n\n    @Override\n    public Class\u003cBook\u003e handledType() {\n        return Book.class;\n    }\n\n    @Override\n    public List\u003cClass\u003c? extends Book\u003e\u003e handledSubTypes() {\n        // Return other types that this serializer handles\n        return Arrays.asList( AudioBook.class, PaperBook.class );\n    }\n    \n    // rest of the serializer ...\n}\n```\n\n\n## Authentication\n\nRequestor features the `Auth` functional interface responsible for authenticating the requests as the last step in the [Request Processing](#request-processing). It delivers the credentials to the request and ultimately sends it. Like any other processor, the Auth interface is an **async callback**. Therefore, after performing the necessary changes in the request, it must call `request.send()` to really send it. Moreover, we may find it advantageous to use the Session's Store to retrieve credential info. Check the following example:\n\n```java\nsession.req(\"/api/authorized-only\")\n        .auth(request -\u003e {\n            // Retrieve the token from the store\n            String userToken = request.getValue(\"userToken\");\n\n            // Provide the credentials in the Authorization header\n            request.setHeader(\"Authorization\", \"Bearer \" + userToken);\n\n            // Your request will be hanging forever if you do not call `send`\n            request.send();\n        });\n```\n\nThis is an example of how to perform a usual authentication. Indeed, this logic is already provided\nby the [`BearerAuth`](#bearer-token) implementation.\n\nNotice that Auth's async nature enables us to do complex stuff before actually providing the credential data\nto the request. We can perform other asynchronous tasks before properly configuring the request. If, for instance,\nwe need to ping another endpoint to grab some token data, we can easily do it. Check the example below:\n\n```java\nAuth myAuth = request -\u003e {\n    // We are reaching another endpoint sending a password to get an updated token\n    request.getSession().req(\"/api/token\")\n            .payload(\"my-password\") // Set `my-password` in the request body\n            .skip(Process.AUTH_REQUEST) // Skip this auth so we don't stuck in an infinite loop\n            .post(String.class) // Send a POST request and receive the body as String\n            .onSuccess(token -\u003e {\n                 // After receiving the updated token, we set it into the request and send it\n                request.setHeader(\"Authorization\", \"Bearer \" + token);\n                request.send();\n            });\n};\n```\n\nWe may do any other useful async task, like performing heavy hash processes using *web workers*, before sending the request.\n\nAdditionally, Requestor allows us to register an Auth `Provider` instead of the `Auth` instance. The Provider is a **factory**\nthat returns an `Auth` instance for each request. It is really valuable when implementing authentication mechanisms\nthat require state management, like the `DigestAuth`. Check an example below of how to register an `Auth.Provider` in\nthe Session:\n\n```java\nsession.setAuth(new Auth.Provider() {\n    public Auth getInstance() {\n        // Supposing you implemented MyAuth elsewhere\n        return new MyAuth();\n    }\n});\n    \n// Lambda syntax\nsession.setAuth(MyAuth::new);\n```\n\n\n### Basic\n\nIn order to facilitate our development, Requestor provides standard Auth implementations. For instance, the `BasicAuth` performs the **basic access authentication** by putting a header field in the form of `Authorization: Basic \u003ccredentials\u003e`, where credentials is the Base64 encoding of `username` and `password` joined by a single colon `:`. It might be helpful to retrieve credentials data from the Session Store, like in the following example:\n\n```java\nUser user = session.getValue(\"user\");\n\nsession.req(\"/api/authorized-only\")\n        .auth(new BasicAuth( user.getUsername(), user.getPassword() ));\n```\n\nBasicAuth optionally accepts a third boolean param called `withCredentials`. It will instruct the \nbrowser to allow cross-site requests. *(gwt only)*\n\n\n### Bearer Token\n\nCorrespondingly, the `BearerAuth` performs the **bearer token authentication** by adding a header to the request in the form of `Authorization: Bearer \u003ctoken\u003e`.\nSee how you can enable this Auth in your Session to all requests using a `Provider`:\n\n```java\nsession.setAuth(() -\u003e {\n    UserInfo userInfo = session.getValue(\"userInfo\");\n    return new BearerAuth(user.getToken());\n});\n```\n\nBearerAuth optionally accepts a second boolean param called `withCredentials`. It will instruct the\nbrowser to allow cross-site requests.\n\n\n### Certificate Authentication\n\nThe `CertAuth` class implements certificate-based authentication to ensure secure communication by\nsetting up an SSL context with the provided certificate details. This method typically involves using\na private key and a certificate chain to authenticate the client to the server. The class can handle\ncertificates either from a file path or directly from an input stream, and it can optionally accept\na TrustPolicy to customize trust management during SSL communication.\n\nTo use CertAuth, you need to provide the path to your certificate or an input stream containing your\ncertificate, and the password for accessing the certificate's key store. The SSL context is set up to\nuse the TLSv1.2 protocol by default.\n\n```java\n// Using a certificate from a file path\nsession.setAuth(new CertAuth(\"/path/to/cert.pem\", \"password\");\n\n// Using a certificate from an InputStream\nInputStream certStream = new FileInputStream(\"/path/to/cert.pem\");\nsession.setAuth(new CertAuth(certStream, \"password\"));\n\n// Including a custom TrustPolicy\nTrustPolicy trustPolicy = new CustomTrustPolicy(); // Define your custom trust policy\nsession.setAuth(new CertAuth(\"/path/to/cert.pem\", \"password\", trustPolicy));\n```\n\n**Parameters:**\n- `certPath` or `certInputStream`: The path to the certificate file or the InputStream containing the certificate.\n- `password`: The password to unlock the key store.\n- trustPolicy (optional): A custom policy to modify the default trust management behavior.\n\n**Exceptions:**\n- `AuthException`: Thrown if there is any issue loading the certificate or setting up the SSL context.\n\nThis class supports loading certificates in the default key store format supported by Java.\nEnsure that your certificates are prepared accordingly to avoid runtime issues.\n\n\n### Digest\n\nRequestor provides a ready-to-use `DigestAuth` supporting **qop** (*auth* or *auth-int*) and \n**md5** hash algorithm.\n\nInstantiate `DigestAuth` in the requests, sessions or services passing the `username`, \n`password` and the `algorithm`. Optionally, there is a fourth boolean `withCredentials` param to\nmake cross-site requests:\n\n```java\nString username = \"username\";\nString password = \"password\";\nString hashAlgo = \"md5\"\nboolean withCredentials = true;\n\nsession.req(\"/api/protected\") \n        .auth(new DigestAuth(user, password, hashAlgo, withCredentials))\n        .get();\n```\n\nWhen registering `DigestAuth` in the [Session](#session) or [Service](#services), it is recommended \nto do it through a `Provider` to avoid sharing the internal Auth state between requests:\n\n```java\n// register a Provider of DigestAuth in the session\nsession.setAuth(() -\u003e new DigestAuth(session.getValue(\"username\"), session.getValue(\"password\"), \"md5\"));\n```\n\n\n### OAuth2 (GWT)\n\nRequestor provides client-side OAuth2 authenticators supporting both *header* and *url query param* \nstrategies. It is made available by the `requestor-oauth2gwt` extension.\n\nTo enable OAuth2 authentication, set the callback url in your oauth provider as `mydomain.com/oauthWindow.html`.  \n\n\nIn order to use it in you app, first install the extension dependency:\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003eio.reinert.requestor.ext\u003c/groupId\u003e\n  \u003cartifactId\u003erequestor-oauth2gwt\u003c/artifactId\u003e\n  \u003cversion\u003e${requestor.version}\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nNext, inherit the `RequestorOAuth2` GWT module in your gwt.xml file:\n\n```xml\n\u003cinherits name=\"io.reinert.requestor.gwt.oauth2.RequestorOAuth2\"/\u003e\n```\n\nThen, according to the required authorization strategy we can either instantiate \n`OAuth2ByHeader` or the `OAuth2ByQueryParam` passing the `authUrl` and the `appClientId`.\nOptionally, we can also inform a sequence of `scopes`: \n\n```java\nsession.req(\"https://externaldomain.com/oauth2\") \n        .auth(new OAuth2ByQueryParam(authUrl, appClientId, scope1, scope2, ...))\n        .get(); \n```\n\nWhen registering an OAuth2 Auth in a [Session](#session) or in a [Service](#services), it is \nrecommended to do it through a `Provider` to avoid sharing the internal Auth state between requests:\n\n```java\n// register a Provider of OAuth2ByHeader in the session\nsession.setAuth(() -\u003e new OAuth2ByHeader(session.getValue(\"authUrl\"), session.getValue(\"appClientId\")));\n```\n\n## Processors (middlewares)\n\nOne of the library's main features is the ability to introduce ***asynchronous hooks*** to \nprocess requests and responses. These middlewares are called **Processors**. Furthermore, it is \nessential to note that each processor kind has a specific capacity that differentiates it from \nothers. Thus, it helps us to organize our application's communication processing stack better.\n\nThe request and response processing are include in the **REQUEST LIFECYCLE** as follows:\n1. User builds and invokes a new **Request** through the **Session**\n2. Request is processed by `RequestFilters`\n3. Request is processed by `RequestSerializer` and serialized\n4. Request is processed by `RequestInterceptors`\n5. Request is processed by `Auth` and *sent* to network\n6. **Response** is *received* from network\n7. Response is processed by `ResponseInterceptors`\n8. Response is processed by `ResponseDeserializer` and deserialized\n9. Response is processed by `ResponseFilters`\n10. User receives the processed Response\n\n![request-lifecycle](https://user-images.githubusercontent.com/1285494/146391548-e44c0c83-5488-4455-a88e-3f7cdf7ff8a4.png)\n\nAll processors are able to manipulate the request/response, but they have the following \ndistinguishing characteristic according to its kind:\n* **Filters** - they can modify the *deserialized* payload\n* **Serializers / Deserializers** - they perform payload serialization and deserialization\n* **Interceptors** - they can modify the *serialized* payload\n\nWe can register as many **Filters** and **Interceptors** as we want in a **Session**. They are \nexecuted in the same order they were registered.\n\nRegarding the **Auth**, there is only one available *per* ***Request***. We can register a default \nAuth in the Session using `session.setAuth(\u003cAuth\u003e)`, but we can override it when building a request.\nRefer to the [Auth](#auth) section for more details.\n\nAs for the **RequestSerializer** and **ResponseDeserializer**, there is only one available *per* \n***Session***. They resort to the **SerializationEngine** to perform serialization and \ndeserialization. The engine holds the registered **Serializers** and is responsible for \nde/serializing objects according to the *class* and *media type*.\n\n### Request Filter\n\nA `RequestFilter` hooks an undergoing deserialized request (`RequestInProcess`) to modify it, even \nits payload, or perform any other action that fits the business requirements triggered by a new\nrequest. As any other **Processor**, the **RequestFilter** is ***asynchronous***, so we must call\n`request.proceed()` to move the request forward in the request processing.\n\nWe can add a `RequestFilter` to the **Session** by calling `session.register(\u003cRequestFilter\u003e)`. \n\n```java\nsession.register(new RequestFilter() {\n    public void filter(RequestInProcess request) {\n        // Access the Store bounded to this request/response lifecycle\n        String encoding = request.getValue(\"encoding\");\n\n        // Modify the request headers\n        request.setHeader(\"Accept-Encoding\", encoding);\n\n        // Modify any other request option, including the payload\n        request.setPayload(null);\n\n        // Call proceed otherwise the request hangs forever\n        request.proceed();\n    }\n});\n```\n\nIn addition to registering a **RequestFilter** instance, we can instead register a `Provider` to \ncreate new filter instances for each new request. This is useful if a filter relies on some \ninternal state that should not be shared among other request lifecycles. See how to register it:\n\n```java\nsession.register(new RequestFilter.Provider() {\n    public RequestFilter getInstance() {\n        // Suposing you implemented MyRequestFilter elsewhere\n        return new MyRequestFilter();\n    }\n});\n\n// Lambda syntax\nsession.register(MyRequestFilter::new); // Same as `session.register(() -\u003e new MyRequestFilter())`\n```\n\nBesides proceeding with the request, we can alternatively ***abort*** it by calling\n`request.abort(\u003cMockResponse\u003e|\u003cRequestAbortException\u003e)`. Check below:\n\n```java\nsession.register(new RequestFilter() {\n    public void filter(RequestInProcess request) {\n        // Abort the request with a fake response\n        request.abort(new MockResponse(Status.BAD_REQUEST));\n    }\n});\n```\n\nIf we abort with `MockResponse` then **load** callbacks are triggered,\nas well **success** and **status** depending on the response status code.\nOtherwise, if the request is aborted with `RequestAbortException`, then **abort** \ncallbacks are triggered.\n\n### Request Serializer\n\nA `RequestSerializer` receives a `SerializableRequestInProcess` along with the\n`SerializationEngine`. It is supposed to serialize the request and proceed with\nthe request processing. The engine uses the registered **Serializers** to serialize \nthe request matching the payload object *class* and the request's *content-type*.\n\nThere is only one `RequestSerializer` per Session, and it can be set with\n`session.setRequestSerializer(\u003cRequestSerializer\u003e)`. Check the default implementation below:\n\n```java\nsession.setRequestSerializer(new RequestSerializer() {\n    public void serialize(SerializableRequestInProcess request,\n                          SerializationEngine engine) {\n        // The engine is capable of serializing the request\n        engine.serializeRequest(request);\n        // It's possible to perform any async task during serialization\n        request.proceed();\n    }\n});\n```\n\n### Request Interceptor\n\nA `RequestInterceptor` hooks an undergoing serialized request (`SerializedRequestInProcess`) to \nmodify it, even its payload, or perform any other action that fits the business requirements \ntriggered by a new request. As any other **Processor**, the **RequestInterceptor** is \n***asynchronous***, so we must call `request.proceed()` to move the request forward in the \nrequest processing.\n\nWe can add a `RequestInterceptor` to the **Session** by calling `session.register(\u003cRequestInterceptor\u003e)`. \n\n```java\nsession.register(new RequestInterceptor() {\n    public void intercept(SerializedRequestInProcess request) {\n        // Access the Store bounded to this request lifecycle\n        String encoding = request.getValue(\"encoding\");\n\n        // Modify the request headers\n        request.setHeader(\"Accept-Encoding\", encoding);\n\n        // Modify any other request option, including the serialized payload\n        String json = request.getSerializedPayload().asString();\n        SerializedPayload jsonp = SerializedPayload.fromText(\")]}',\\\\n\" + json);\n        request.setSerializedPayload(jsonp);\n\n        // Call proceed otherwise the request hangs forever\n        request.proceed();\n    }\n});\n```\n\nIn addition to registering a **RequestInterceptor** instance, we can instead register a `Provider` to \ncreate new interceptor instances for each new request. This is useful if an interceptor relies \non some internal state that should not be shared among other request lifecycles. See how to \nregister it:\n\n```java\nsession.register(new RequestInterceptor.Provider() {\n    public RequestInterceptor getInstance() {\n        // Suposing you implemented MyRequestInterceptor elsewhere\n        return new MyRequestInterceptor();\n    }\n});\n\n// Lambda syntax\nsession.register(MyRequestInterceptor::new); // Same as `session.register(() -\u003e new MyRequestInterceptor())`\n```\n\nBesides proceeding with the request, we can alternatively ***abort*** it by calling\n`request.abort(\u003cMockResponse\u003e|\u003cRequestAbortException\u003e)`. Check below:\n\n```java\nsession.register(new RequestInterceptor() {\n    public void intercept(SerializedRequestInProcess request) {\n        // Abort the request with an exception\n        request.abort(new RequestException(request, \"Manually aborted\"));\n    }\n});\n```\n\nIf we abort with `MockResponse` then **load** callbacks are triggered,\nas well **success** and **status** depending on the response status code.\nOtherwise, if the request is aborted with `RequestAbortException`, then **abort** \ncallbacks are triggered.\n\n### Response Interceptor\n\nA `ResponseInterceptor` hooks an incoming serialized response (`SerializedResponseInProcess`) to\nmodify it, even its serialized payload, or perform any other action that fits the business \nrequirements triggered by a new response. As any other **Processor**, the **ResponseInterceptor**\nis ***asynchronous***, so we must call `response.proceed()` to move the response forward in the\nresponse processing.\n\nWe can add a `ResponseInterceptor` to the **Session** by calling `session.register(\u003cResponseInterceptor\u003e)`.\n\n```java\nsession.register(new ResponseInterceptor() {\n    public void intercept(SerializedResponseInProcess response) {\n        // Modify the response headers\n        response.setHeader(new ContentTypeHeader(\"application/json\"));\n\n        // Modify any other response option, including the serialized payload\n        String jsonp = response.getSerializedPayload().asString();\n        SerializedPayload json = SerializedPayload.fromText(jsonp.substring(8));\n        response.setSerializedPayload(json);\n\n        // Call proceed otherwise the response hangs forever\n        response.proceed();\n    }\n});\n```\n\nIn addition to registering a **ResponseInterceptor** instance, we can instead register a `Provider` to\ncreate new interceptor instances for each new response. This is useful if an interceptor relies \non some internal state that should not be shared among other request lifecycles. See how to \nregister it:\n\n```java\nsession.register(new ResponseInterceptor.Provider() {\n    public ResponseInterceptor getInstance() {\n        // Suposing you implemented MyResponseInterceptor elsewhere\n        return new MyResponseInterceptor();\n    }\n});\n\n// Lambda syntax\nsession.register(MyResponseInterceptor::new); // Same as `session.register(() -\u003e new MyResponseInterceptor())`\n```\n\n### Response Deserializer\n\nA `ResponseDeserializer` receives a `DeserializableResponseInProcess` along with the\n`SerializationEngine`. It is supposed to deserialize the response and proceed with\nthe response processing. The engine uses the registered **Deserializers** to deserialize\nthe response matching the asked payload *class* and the response's *content-type*.\n\nThere is only one `ResponseDeserializer` per Session, and it can be set with\n`session.setResponseDeserializer(\u003cResponseDeserializer\u003e)`. Check the default implementation below:\n\n```java\nsession.setResponseDeserializer(new ResponseDeserializer() {\n    public void deserialize(DeserializableResponseInProcess response,\n                            SerializationEngine engine) {\n        // The engine is capable of deserializing the response\n        engine.deserializeResponse(response);\n        \n        // It's possible to perform any async task during deserialization before proceeding\n        response.proceed();\n    }\n});\n```\n\n### Response Filter\n\nA `ResponseFilter` hooks an incoming deserialized response (`ResponseInProcess`) to\nmodify it, even its serialized payload, or perform any other action that fits the business\nrequirements triggered by a new response. As any other **Processor**, the **ResponseFilter**\nis ***asynchronous***, so we must call `response.proceed()` to move the response forward in the\nresponse processing.\n\nWe can add a `ResponseFilter` to the **Session** by calling `session.register(\u003cResponseFilter\u003e)`.\n\n```java\nsession.register(new ResponseFilter() {\n    public void filter(ResponseInProcess response) {\n        if (!response.hasHeader(\"Custom-Header\"))\n            response.setHeader(\"Custom-Header\", \"Added after response was received\");\n\n        // Check if the caller requested to deserialize the payload as String\n        if (response.getPayloadType().getType() == String.class) {\n            String payload = response.getPayload().asObject();\n            response.setPayload(payload + \"\\nWE JUST MODIFIED THE PAYLOAD!\");\n        }\n\n        // Call proceed otherwise the response hangs forever\n        response.proceed();\n    }\n});\n```\n\nIn addition to registering a **ResponseFilter** instance, we can instead register a `Provider` to\ncreate new filter instances for each new response. This is useful if a filter relies\non some internal state that should not be shared among other request lifecycles. See how to\nregister it:\n\n```java\nsession.register(new ResponseFilter.Provider() {\n    public ResponseFilter getInstance() {\n        // Suposing you implemented MyResponseFilter elsewhere\n        return new MyResponseFilter();\n    }\n});\n\n// Lambda syntax\nsession.register(MyResponseFilter::new); // Same as `session.register(() -\u003e new MyResponseFilter())`\n```\n\n## Session\n\nRequestor is a session-based HTTP client. It means that a **Session** ties every configuration and action a user can take related to communication. Thus, the `Session` object is the baseline of every communication process in the application. What is better, Requestor does not restrict its users to having only one global Session. We can have ***as many sessions as it makes sense*** according to our business requirements. For example, if we are building a modern client app, we may communicate with different microservices. It may be reasonable to have one Session for each microservice with different configurations. This flexibility promotes a much more **reliable and maintainable code** since we can isolate different business logics in their own context, avoiding runtime conflicts and undesirable multi-path coding.\n\nTo instantiate a new `Session`, we can call `Requestor.newSession()`.\n\n```java\nSession session = Requestor.newSession();\n```\n\nBesides allowing registration of many [Processors](#processors-middlewares), the Session admits setting many default request options. Along with that, it is possible to reset the Session state.\n\n```java\nsession.reset();\n```\n\nThis method will reset all the Session's request options to their default values.\n\n### Session's Thread Pool\n\nThe async-first aspect of Requestor in JVM runtime is enabled by the usage of multiple threads.\nA `Session` is backed by an `AsyncRunner`. For JVM, Requestor provides the `ScheduledExecutorAsyncRunner`\nwhich is powered by the `ScheduledExecutorService` interface. By default, the ScheduledExecutorAsyncRunner \ninstantiates a `ScheduledThreadPoolExecutor` with 10 threads as the core pool size. When creating a new `Session`,\nwe can pass a `ScheduledExecutorAsyncRunner` instance with our own ScheduledExecutorService instance if we want a\ndifferent configuration.\n\nExample creating a Session passing a custom Thread Pool:\n\n```java\nint corePoolSize = 20;\nScheduledExecutorService threadPool = Executors.newScheduledThreadPool(corePoolSize);\nAsyncRunner asyncRunner = new ScheduledExecutorAsyncRunner(threadPool);\n\nSession session = Requestor.newSession(asyncRunner);\n```\n\nThe `AsyncRunner` interface exposes the `shutdown()` method that allow us to shut down the underlying thread pool.\nThis method is also expose by the `Session` which delegates to the underlying `AsyncRunner`.\n\n**💡 PRO TIP**: Often people question themselves what would be the optimal thread pool size.\nThere's no simple answer to it, and we recommend you read the article\n[\"How to set an ideal thread pool size\"](https://engineering.zalando.com/posts/2019/04/how-to-set-an-ideal-thread-pool-size.html)\nfrom Anton Ilinchik. But if you want a quick formula to start, use the following:\n\n```text\nNumber of Threads = Number of Available Cores * (1 + (Wait time / Service time))\n```\n\n* **Wait time** - is the time spent waiting for ***remote tasks*** to complete.\n  * waiting for the HTTP response from a web service\n  * waiting for the OS to process some task outside your app\n* **Service time** - is the time spent processing ***local tasks*** in your app.\n  * processing the received HTTP response or OS signal doing things like deserialization, transformations, etc.\n\nExample (from the article): A worker thread makes a call to a microservice, serializes response into JSON and executes\nsome set of rules. The microservice response time is 50ms, processing time is 5ms. We deploy our application to a server\nwith a dual-core CPU\n\n```text\nThreadPoolSize = 2 * (1 + (50 / 5)) = 22\n```\n\n**👀 HEADS UP**: Currently in the JVM platform we are limited regarding the number of threads we can work with\ndue to the hard link between the threads and the OS. But, there's a revolution in Java multi-threading coming soon!\nThe [Project Loom](https://blogs.oracle.com/javamagazine/post/going-inside-javas-project-loom-and-virtual-threads)\nis working hard to bring into the Java world the concept of [**Virtual Threads**](https://en.wikipedia.org/wiki/Virtual_threads)\nwhich don't demand the OS to allocate a hard resource for each thread. Once we have it available, we wil be able to\nhandle thousands (even millions) of co-living threads without overheading the OS and we won't need to worry about\nthe thread pool size anymore. Requestor is prepared to work with Virtual Threads. As soon they are delivered to the\nJDK, Requestor users will experiment a drastic performance gain by acquiring the ability to fire numerous\nrequests concurrently at a much lower cost.\n\n#### 🔥 How to use Virtual Threads with Requestor? (JDK19+)\n\n**JDK 19** allows us to use **Virtual Threads** as a preview feature. In order to instantiate a `Session` backed by a pool\nof virtual threads, see the code below:\n\n```java\n// Instantiate a ScheduledExecutorService with max core pool size and a virtual thread factory\nScheduledThreadPoolExecutor threadPool = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(\n        Integer.MAX_VALUE,\n        Thread.ofVirtual().factory());\n\n// Force the core threads to be destroyed after they become idle (reach keep alive time)\nthreadPool.allowCoreThreadTimeOut(true);\n\n// Optionally set the keep alive time for; default is 10ms\nthreadPool.setKeepAliveTime(10L, TimeUnit.MILLISECONDS);\n\n// Instantiate a Requestor Session with this virtual thread pool wrapped in a ScheduledExecutorAsyncRunner\nSession session = Requestor.newSession(new ScheduledExecutorAsyncRunner(threadPool));\n```\n\n**NOTE:** Add the VM option `--enable-preview` when running the JDK19 program in order to access the virtual threads.\n\n### Session's Request Options\n\nThe same request options made available by the `Session` are also provided by in any [`Service`](#services).\n\n#### Media-Type\n\nThis session configuration will be applied to every request's Content-Type and Accept headers.\n\n```java\nsession.setMediaType(\"application/json\");\n```\n\n#### Headers\n\nWe can set any header to be shared among all session's requests.\n\n```java\n// Set a header into the session\nsession.setHeader(\"Accept-Encoding\", \"gzip\");\n\n// Get a header from the session\nHeader header = session.getHeader(\"Accept-Encoding\");\n\n// Remove a header from the session\nboolean removed = session.delHeader(\"Accept-Encoding\");\n```\n\n#### Auth\n\nThis session configuration will be applied to every request's [`auth`](#auth) option. We can \neither register an `Auth` instance or a `Provider`.\n\n```java\n// The same Auth instance will be used by all requests\nsession.setAuth( new BearerAuth(session.getValue(\"token\")) );\n\n// By setting a Provider, each request will have a new Auth instance\n// Helpful to restrict the Auth's internal state to the request lifecycle\nsession.setAuth( () -\u003e new BearerAuth(session.getValue(\"token\")) );\n```\n\n#### Timeout\n\nThis session configuration will be applied to every request's [`timeout`](#timeout) option.\n\n```java\n// Every request will have a timeout of 20s\nsession.setTimeout(20_000);\n```\n\n#### Delay\n\nThis session configuration will be applied to every request's [`delay`](#delay) option.\n\n```java\n// Every request will have a delay of 3s\nsession.setDelay(3_000);\n```\n\n#### Retry\n\nThis session configuration will be applied to every request's [`retry`](#retry) option.\n\n```java\n// Every request will have a retry policy of 5s, 10s and 30s on 429 responses\nsession.setRetry(DelaySequence.fixed(5, 10, 30), Status.TOO_MANY_REQUESTS);\n```\n\n```java\n// Every request will have a new instance of MyRetryPolicy\nsession.setRetry(MyRetryPolicy::new);\n```\n\n### Requesting\n\nThe Session is the starting point to build requests. We access the request builder by calling `session.req(\u003cUri\u003e)`. Since the builder is also an invoker, we can call an invoke method any time to send the request. The invoke methods are named according to the respective HTTP methods they claim. All those methods are chainable, as demonstrated below:\n\n```java\n// Start requesting informing the URI\nRequestInvoker req = session.req(\"/api/book/1\");\n\n// Set up the request\nreq = req.timeout(5_000);\n\n// Invoke the request\nRequest\u003cBook\u003e request = req.get(Book.class);\n\n// All together\nRequest\u003cBook\u003e request = session.req(\"/api/book/1\")\n        .timeout(5_000)\n        .get(Book.class);\n```\n\nIn order to better understand the requesting mechanics, refer to the [Requesting Fluent API](#requesting-fluent-api) section.\n\nIn addition to the fluent request builder exposed by the `req` method, the Session features **direct invoker methods** that quickly dispatch requests.\nThese are named likewise the HTTP Methods they invoke.\n\n```java\nBook book = new Book(\"Clean Code\", \"Robert C. Martin\");\n\n// Same as `session.req(\"/api/books\").payload(book).post(Book.class)`\nRequest\u003cBook\u003e postReq = session.post(\"/api/books\", book, Book.class);\n\n// Same as `session.req(\"/api/books\").get(List.class, Book.class)`\nRequest\u003cCollection\u003cBook\u003e\u003e getReq = session.get(\"/api/books\", List.class, Book.class);\n```\n\n### DeferredPool Factory\n\nAnother convenient feature is the possibility of instantiating a Session with a customized `DeferredPool.Factory`. This factory provides `Deferred` instances to the request dispatcher, returning a `Request` to the Session's user. Thus, we can immediately add some global callbacks to keep our code DRY when generating a Deferred instance.\n\nThe example below demonstrates a customized DeferredPool Factory that fires a `ShowLoadingEvent` right before the request is sent and fires a `HideLoadingEvent` once the request gets [loaded](#event-driven-callbacks) ('load' event) or [interrupted](#event-driven-callbacks) ('error' event).\n\n```java\nclass AppDeferredPoolFactory implements DeferredPool.Factory {\n\n    public \u003cT\u003e DeferredPool\u003cT\u003e create(SerializedRequest request, AsyncRunner asyncRunner) {\n        final DeferredPollingRequest\u003cT\u003e deferred = new DeferredPollingRequest\u003cT\u003e(request, asyncRunner);\n\n        // Show loading widget before sending the request\n        APP_FACTORY.getEventBus().fireEvent(new ShowLoadingEvent());\n\n        // Hide loading widget on load or abort\n        deferred.onLoad(() -\u003e APP_FACTORY.getEventBus().fireEvent(new HideLoadingEvent()))\n                .onError(() -\u003e APP_FACTORY.getEventBus().fireEvent(new HideLoadingEvent()));\n\n        return deferred;\n    }\n}\n```\n\nTherefore, we can instantiate our Session with this customized Deferred Pool Factory, as demonstrated below:\n\n```java\nSession session = Requestor.newSession(new AppDeferredPoolFactory());\n```\n\n\n## Store\n\nRequestor features a component to save and retrieve objects by key: the `Store`. The `Session`, the `Service`, the `Request`, and the `Response` types extend the `Store` interface so that all of them provide such functionality.\n\nWhen a Store is generated from another store, it is linked to the previous Store in a tree structure. This structure allows the Stores to maintain their own scope of saved data while delegating queries to the parents when they do not find an object.\n\nFor example, if we create a Request from a Session, the Request is a Store linked to the Session Store. Differently, if we create a Request from a Service, the Request Store is linked to the Service Store. In its turn, since a Service is created from a Session, the Service Store is linked to the Session Store.\n\nAlso, note that when a Request produces a Response, they share the same Store. So there is one Store only in a Request-Response lifecycle.\n\nFinally, a Store emits some important events that allow us to react when modifications happen to it.\n* **Saved Event** - it's fired when a new data is saved into the store.\n  * It provides us access to the `newData` that's being saved and also the `oldData` that was in the key's slot before (`null` if there was none).\n  * We can listen to this event by registering a handler with the `onSaved` method.\n* **Removed Event** - it's fired when a data is removed from the store.\n    * It provides us access to the `oldData` that's being removed.\n    * We can listen to this event by registering a handler with the `onRemoved` method.\n* **Expired Event** - it's fired when a data TTL expires.\n    * It provides us access to the `oldData` that has expired.\n    * We can listen to this event by registering a handler with the `onExpired` method.\n    * By default, after expiration, the data is removed, thus firing the removed event also.\n      * Not wanting every data to be automatically removed, we can call `store.save(Store.REMOVE_ON_EXPIRED_DISABLED, true)`\n      * We can alternatively refresh the data in the onExpired handler to avoid it getting deleted with `event.getStore().refresh(key)` or `event.getStore().refresh(key, ttl)` (to also set a new TTL)\n\nAll handlers provide a `cancel()` method that we can call to deregister (unsubscribe) the handler from the store.\n\nA Store provides the following operations:\n\n```java\ninterface Store {\n\n    // Retrieves the value of the data saved with the given key.\n    // If no data is found in the local store, the upstream stores are queried.\n    \u003cT\u003e T getValue(String key);\n    \n    // Retrieves the data object saved with the given key.\n    // If no data is found in the local store, the upstream stores are queried.\n    Data getData(String key);\n\n    // Saves the value into the store associating with the key.\n    // Returns the same store to enable method chaining.\n    // Fires the onSaved event.\n    Store save(String key, Object value);\n\n    // Saves the value associating with the key in an upstream store according to the specified level.\n    // To persist in the immediate parent store, set the level param to Level.PARENT.\n    // To persist in the root upstream store, set the level param to Level.ROOT.\n    Store save(String key, Object value, Level level);\n\n    // Saves data into the store with a time-to-live (TTL) period in milliseconds.\n    // After the TTL expires, the onExpired event is fired and the data is removed.\n    // Since the data is removed, the onRemoved event is also fired.\n    Store save(String key, Object value, long ttlMillis);\n\n    // Saves data with a time-to-live (TTL) period into an upstream store.\n    Store save(String key, Object value, long ttlMillis, Level level);\n\n    // Checks if there's an object associated with the given key.\n    // If no data is found in the local store, the upstream stores are queried.\n    boolean exists(String key);\n\n    // Checks if there's an object associated with the given key and if it's equals to the given value.\n    // If no data is found in the local store, the upstream stores are queried.\n    boolean exists(String key, Object value);\n\n    // Deletes the object associated with this key if it owns such record.\n    // This method affects only the local store. (It's never delegated to the upstream stores)\n    // Returns the data that was removed or `null` if there was no data associated with the given key.\n    // Fires the onRemoved event.\n    Data remove(String key);\n    \n    // Refreshes the data saved with this key extending its valid time for the given TTL.\n    // It affects only the local store, i.e., it's not residually executed in the upstream stores.\n    // Returns the data that was refreshed or `null` if there was no data associated with the given key.\n    Data refresh(String key, long ttlMillis);\n    \n    // Refreshes the data saved with this key extending its valid time for its original TTL.\n    Data refresh(String key);\n\n    // Clears all data owned by this Store.\n    void clear();\n\n    // Clears all data owned by this Store.\n    // If you don't want the onRemoved handlers to be triggered then set fireRemovedEvent to false.\n    void clear(boolean fireRemovedEvent);\n\n    // Registers a handler to be executed AFTER a new data is SAVED into the local store.\n    Store onSaved(String key, Handler handler);\n\n    // Registers a handler to be executed AFTER a new data is REMOVED from the local store.\n    Store onRemoved(String key, Handler handler);\n\n    // Registers a handler to be executed AFTER the data TTL EXPIRES.\n    Store onExpired(String key, Handler handler);\n}\n```\n\nIn summary:\n* A `Session` ***is*** a store (extends the `Store` interface) with no stores attached to it (i.e. a **Root Store**).\n* A `Service` ***is*** a store derived from another store (specifically a `Session`). Hence, it is a **Leaf Store** with a **Session** as it's parent (and root) store. \n* A `Request` (and it's respective `Response`) ***is*** a **Leaf Store** derived either from a `Session` or a `Service`.\n  * If a **Request** is created from a **Session** directly, then the **Session** is its **PARENT and ROOT** store.\n  * If a **Request** is created from a **Service** instead, then\n    * the **Service** is its **PARENT** store and\n    * the **Session** is its **ROOT** store.\n\n### Session Store\n\nSince a Session is not linked to any parent, it is a Root Store.\nWith it, we can save, retrieve and remove objects by key.\n\n```java\n// Save an object in the session store\nsession.save(\"key\", anyObject);\n\n// Get an object from the session store\n// Automatically typecasts to the requested type\nAnyType object = session.getValue(\"key\");\n        \n// Check if there's an object with that key\nboolean isSaved = session.exists(\"key\");\n\n// Check if there's an object with that key equals to the given value\nboolean isEquals = session.exists(\"key\", anyObject);\n\n// Remove the object from the session store\nboolean isRemoved = session.remove(\"key\");\n```\n\n### Request Store\n\nBoth the Request and the Response is a Store available during the [Request Lifecycle](#processors-middlewares) and accessed within the [Processors](#processors-middlewares).\n\nHaving a transient **Request Store** is helpful to share information among **Processors** without cluttering the **Session Store** or **Service Store** that generated it.\n\nThe Request Store provides access to the upstream Stores' data. We can even persist data from the Request Store into the parent Stores, though we cannot delete data from them.\n\nWhen we call `request.getValue(\u003ckey\u003e)`, the Request Store first tries to retrieve the associated object from its own scope. Not finding, it queries the parent Store, and so on until it reaches the Root Store. Also, the result is automatically typecasted to the requested type.\n\n```java\n// Get an object from the store or the deriving store\n// Automatically typecasts the result\nAnyType object = request.getValue(\"key\");\n```\n\nThe same underlying Store is shared by a Request and a Response. Hence, we can also access the Request scope Store from the Response, sharing data in a single Request-Response lifecycle.\n\n```java\nsession.req(\"/server\")\n       .save(\"hidden\", true) // save data in the request store\n       .get()\n       .onLoad(response -\u003e {\n            // retrieve data from the request store\n            if (response.exists(\"hidden\", true)) {\n                // do something...\n            }\n        });\n```\n\nTo save an object locally, we call `request.save(\u003ckey\u003e, \u003cobject\u003e)`.\nDifferently, to save an object in an upstream Store, we call `request.save(\u003ckey\u003e, \u003cobject\u003e, \u003clevel\u003e)`.\nIf the request was originated from a [Service](#services) and we set the level as `Level.PARENT`, then the data is saved in the Service Store.\nBut if we set the level as `Level.ROOT`, the data is saved in the Session Store that is linked to the Service.\nIn the other hand, if the request was directly originated from a [Session](#session), then both `Level.PARENT` and `Level.ROOT` will cause the same effect of saving in the Session Store, because the Request's parent and root Stores are the same.\n\n```java\n/* SESSION scenario */\nSession session = getSession();\n\nsession.get(\"/server\")\n       .onLoad(response -\u003e {\n            // Save an object in the parent store\n            // Since this is a session's request, data is saved in the Session Store\n            response.save(\"key\", anyObject, Level.PARENT);\n\n            // Save an object in the root store\n            // Since this is a session's request, it has the same effect of the previous\n            response.save(\"key\", anyObject, Level.ROOT);\n        });\n```\n\n```java\n/* SERVICE scenario */\nService service = getService();\n\nservice.get()\n       .onLoad(response -\u003e {\n           // Save an object in the parent store\n           // Since this is a service's request, data is saved in the Service Store\n           response.save(\"key\", anyObject, Level.PARENT);\n\n           // Save an object in the root store\n           // It will save in the Session Store from which the Service was created\n           response.save(\"key\", anyObject, Level.ROOT);\n       });\n```\n\nTo delete a local record, we call `request.remove(\u003ckey\u003e)`. We cannot remove objects from the upstream Stores.\n\n```java\n// Delete the record associated with the given key\nrequest.remove(\"key\");\n\n// The response share the same store with the request\nresponse.remove(\"key\");\n```\n\n**💡 PRO TIP**: Request scope store is specially useful to deal with exceptional cases in [request/response processors](#processors-middlewares).\nFor instance, suppose you created processors to show a loading widget when requesting and hide when the response is received or an error occurs.\nBut, for some reason, you want to make 'hidden' requests, so that the loading widget is not shown.\nYou can then call `.save(\"hidden\", true)` when building the request and check for this flag in the processors by calling `.exists(\"hidden\", true)` to skip displaying the loading widget.\nRequestor's [showcase app](https://reinert.github.io/requestor/latest/examples/showcase) implements such scenario.\n[Here](https://github.com/reinert/requestor/blob/master/examples/requestor-showcase/src/main/java/io/reinert/requestor/examples/showcase/Showcase.java#L80) a hidden ping request is executed to wake-up the server,\nand [here](https://github.com/reinert/requestor/blob/master/examples/requestor-showcase/src/main/java/io/reinert/requestor/examples/showcase/ShowcaseDeferredFactory.java#L44) the Request Store is queried to skip showing the loading widget.\n\n### Service Store\n\nA Service is a Store derived from the Session Store.\n\nHaving a **Service Store** is helpful to share information in the context of that Service only.\n\nThe Service Store provides access to the parent Session Store's data. We can even persist data from the Service Store into the Session Store, though we cannot delete Session's data from it.\n\nWhen we call `service.getValue(\u003ckey\u003e)`, the Service Store first tries to retrieve the associated object from its own scope. Not finding, it queries the parent Session Store. Also, the result is automatically typecasted to the requested type.\n\n```java\n// Get an object from the store or the deriving session store\n// Automatically typecasts the result\nAnyType object = service.getValue(\"key\");\n```\n\nTo save an object locally, we call `service.save(\u003ckey\u003e, \u003cobject\u003e)`.\nDifferently, to save an object in the parent Session Store, we call `service.save(\u003ckey\u003e, \u003cobject\u003e, \u003clevel\u003e)`.\n\n```java\nService service = getService();\n\n// Save an object in service scope only\nservice.save(\"key\", anyObject);\n\n// Save an object in the parent session store\n// Level.PARENT causes the same effect here\nservice.save(\"key\", anyObject, Level.ROOT);\n```\n\nTo delete a local record, we call `service.remove(\u003ckey\u003e)`. We cannot delete records from the parent Session Store.\n\n```java\n// Delete the record associated with the given key\nservice.remove(\"key\");\n```\n\n\n## Services\n\nRequestor introduces the **Service** concept to represent a ***subject-oriented client***. It \nmeans that a Service should bind together closely related network operations according to some \ncriteria. In this sense, if we are consuming a REST API, we can build *Entity- or \nResource-oriented* Services. Analogously, when consuming RPC APIs, we can create *Feature- or \nGroup-oriented* Services.\n\nThe `Service` is a **session branch** derived from the main [Session](#session) that holds local \nconfigurations but residually leverages the main's. Within this coordinated context, we can \ndefine configurations only related to the target subject without cluttering the main context. In \nother words, the Service's [Request Options](#request-options) and the [Store](#store) are \nindependent of the Session's and have preferential usage over it.\n\nIn order to create a **Service**, we need to extend the `BaseService` class and implement \nthe network operations related to the server API's subject.\n\n### Extending BaseService\n\nRequestor follows a design princ","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freinert%2Frequestor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freinert%2Frequestor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freinert%2Frequestor/lists"}