|
| 1 | +A Pipeline in the Azure SDKs is a collection of policies that can work on the request before sending it to the server and also on the raw responses before handing them to the SDK users. This document describes in detail the structure and functionality of the Pipeline and Policies |
| 2 | + |
| 3 | +# Policies |
| 4 | + |
| 5 | +A pipeline policy manipulates a request as it travels through the pipeline. It is conceptually a middleware that is allowed to modify the request before it is made as well as the response when it is received. |
| 6 | + |
| 7 | +It is important to note that the **first** policy to touch the request will be the **last** policy to work on the response. |
| 8 | + |
| 9 | +[Find out more about the Middleware pattern](https://docs.microsoft.com/aspnet/core/fundamentals/middleware/?tabs=aspnetcore2x&view=aspnetcore-6.0) |
| 10 | + |
| 11 | +```typescript |
| 12 | +export type SendRequest = (request: PipelineRequest) => Promise<PipelineResponse>; |
| 13 | + |
| 14 | +export interface PipelinePolicy { |
| 15 | + name: string; |
| 16 | + sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse>; |
| 17 | +} |
| 18 | +``` |
| 19 | + |
| 20 | +# Pipelines |
| 21 | + |
| 22 | +A `Pipeline` is composed of several policies and provides the ability to make a request that will flow through the policies in a specific order determined by their requirements. This allows manipulating each request before it is sent to the server and to work on the response after receiving it. |
| 23 | + |
| 24 | +```typescript |
| 25 | +export interface AddPipelineOptions { |
| 26 | + afterPhase?: PipelinePhase; |
| 27 | + afterPolicies?: string[]; |
| 28 | + beforePolicies?: string[]; |
| 29 | + phase?: PipelinePhase; |
| 30 | +} |
| 31 | + |
| 32 | +export interface Pipeline { |
| 33 | + addPolicy(policy: PipelinePolicy, options?: AddPipelineOptions): void; |
| 34 | + clone(): Pipeline; |
| 35 | + getOrderedPolicies(): PipelinePolicy[]; |
| 36 | + removePolicy(options: { name?: string; phase?: PipelinePhase }): PipelinePolicy[]; |
| 37 | + sendRequest(httpClient: HttpClient, request: PipelineRequest): Promise<PipelineResponse>; |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +**Note**: It is important to never rely on policy insert ordering and always express policy execution constraints using `AddPipelineOptions` when policies have interactions. For example, the following code snippets have identical effects: |
| 42 | + |
| 43 | +```typescript |
| 44 | +pipeline.addPolicy(barPolicy); |
| 45 | +pipeline.addPolicy(fooPolicy, { afterPolicy: "barPolicy" }); |
| 46 | + |
| 47 | +// The below is the same as the above. |
| 48 | +// Policies don't have to be part of the pipeline in order to be |
| 49 | +// referenced by before/afterPolicy. |
| 50 | +pipeline.addPolicy(fooPolicy, { afterPolicy: "barPolicy" }); |
| 51 | +pipeline.addPolicy(barPolicy); |
| 52 | +``` |
| 53 | + |
| 54 | +Phases are predefined buckets of activity that pre-defined policies inside core will fall into. They are currently a fixed set and not customizable by pipeline consumers. |
| 55 | + |
| 56 | +```typescript |
| 57 | +export type PipelinePhase = "Deserialize" | "Serialize" | "Retry" | "Sign"; |
| 58 | +``` |
| 59 | + |
| 60 | +The execution order is: |
| 61 | + |
| 62 | +1. **Serialize Phase** - Policies that assamble the request to be sent through HTTP, i.e. transforming the body object to its JSON string representation. |
| 63 | +2. **Policies not in a phase** - Any policies added to the pipeline without a specified phase |
| 64 | +3. **Deserialize Phase** - Policies that transform the raw HTTP response into friendlier response shapes. In addition to transforming the body JSON string into a JavaScript object, this can also include things like creating native JS `Date` objects from strings and other object forms not natively supported by JSON. |
| 65 | +4. **Retry Phase** - Policies that inspect the raw response, typically the response code and errors returned from the service to decide whether or not to re-try the request. |
| 66 | +5. **Sign Phase** - Policies that sign the request for security purposes, for example adding a HMAC signature to prevent tampering during transport.``` |
| 67 | + |
| 68 | +# Default Pipeline |
| 69 | + |
| 70 | +`@azure/core-rest-pipeline` exposes `createPipelineFromOptions` which creates pipeline with a predefined set of policies. The resulting pipeline can be configured through `PipelineOptions` |
| 71 | + |
| 72 | +```typescript |
| 73 | +/** |
| 74 | + * Defines options that are used to configure the HTTP pipeline for |
| 75 | + * an SDK client. |
| 76 | + */ |
| 77 | +export interface PipelineOptions { |
| 78 | + /** |
| 79 | + * Options that control how to retry failed requests. |
| 80 | + */ |
| 81 | + retryOptions?: PipelineRetryOptions; |
| 82 | + |
| 83 | + /** |
| 84 | + * Options to configure a proxy for outgoing requests. |
| 85 | + */ |
| 86 | + proxyOptions?: ProxySettings; |
| 87 | + |
| 88 | + /** |
| 89 | + * Options for how redirect responses are handled. |
| 90 | + */ |
| 91 | + redirectOptions?: RedirectPolicyOptions; |
| 92 | + |
| 93 | + /** |
| 94 | + * Options for adding user agent details to outgoing requests. |
| 95 | + */ |
| 96 | + userAgentOptions?: UserAgentPolicyOptions; |
| 97 | +} |
| 98 | + |
| 99 | +/** |
| 100 | + * Defines options that are used to configure internal options of |
| 101 | + * the HTTP pipeline for an SDK client. |
| 102 | + */ |
| 103 | +export interface InternalPipelineOptions extends PipelineOptions { |
| 104 | + /** |
| 105 | + * Options to configure request/response logging. |
| 106 | + */ |
| 107 | + loggingOptions?: LogPolicyOptions; |
| 108 | +} |
| 109 | + |
| 110 | +/** |
| 111 | + * Create a new pipeline with a default set of customizable policies. |
| 112 | + * @param options - Options to configure a custom pipeline. |
| 113 | + */ |
| 114 | +declare function createPipelineFromOptions(options: InternalPipelineOptions): Pipeline; |
| 115 | +``` |
| 116 | + |
| 117 | +**Note:** `InternalPipelineOptions` is used internally by SDK client authors to configure the SDK pipeline but it is not exposed to consumers. Consumers can configure the default pipeline through the `PipelineOptions` when instantiating a new client. |
| 118 | + |
| 119 | +### Policies included in a default pipeline |
| 120 | + |
| 121 | +- proxyPolicy (**NodeJS Only**) |
| 122 | + - A policy that allows one to apply proxy settings to all requests. If not passed static settings, they will be retrieved from the HTTPS_PROXY or HTTP_PROXY environment variables. |
| 123 | +- decompressResponsePolicy (**NodeJS Only**) |
| 124 | + - A policy to enable response decompression according to Accept-Encoding header. |
| 125 | +- formDataPolicy |
| 126 | + - A policy that encodes FormData on the request into the body. |
| 127 | +- userAgentPolicy |
| 128 | + - A policy that sets the User-Agent header (or equivalent) to reflect the SDK version and device runtime. |
| 129 | +- setClientRequestIdPolicy |
| 130 | + - A policy that tags each outgoing request with a unique ID for tracing purposes. This can be useful when opening support tickets against Azure services. |
| 131 | +- defaultRetryPolicy |
| 132 | + - A policy that retries according to three strategies: |
| 133 | + - When the server sends a 429 response with a Retry-After header. |
| 134 | + - When there are errors in the underlying transport layer (e.g. DNS lookup failures.) |
| 135 | + - After considering the above, if the outgoing request fails with a non-final response code, it will retry with an exponentially increasing delay. |
| 136 | +- tracingPolicy |
| 137 | + - A policy to create [OpenTelemetry](https://opentelemetry.io/) tracing spans for each request made by the pipeline. Requests made without a valid tracing context will not be recorded. SDK client authors are expected to use the appropriate callbacks from `@azure/core-tracing` to set tracing contexts for all client operations. |
| 138 | +- redirectPolicy |
| 139 | + - A policy to follow Location headers from the server in order to support server-side redirection. |
| 140 | +- logPolicy |
| 141 | + - A policy that logs all requests and responses using [@azure/logger](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/logger). |
| 142 | + |
| 143 | +There are other policies commonly added to the default pipeline: |
| 144 | + |
| 145 | +- serializationPolicy |
| 146 | + - This policy handles assembling the request body and headers using an OperationSpec and OperationArguments on the request. |
| 147 | +- deserializationPolicy |
| 148 | + - This policy handles parsing out responses according to OperationSpecs on the request. |
| 149 | + |
| 150 | +# Authentication and Signing |
| 151 | + |
| 152 | +Pipeline policies are used to authenticate requests in Azure SDKs. An authentication policy would sign the request before sending it with the required authentication information, these policies run in the `Sign` phase. |
| 153 | + |
| 154 | +For example, the `bearerTokenAuthenticationPolicy` would sign the request by adding the bearer token in the request headers before the request is sent. |
| 155 | + |
| 156 | +# Configuring client pipeline |
| 157 | + |
| 158 | +When instantiating a new client a pipeline can be configured by adding additional policies to the default pipeline, passing a static array of policies. |
| 159 | + |
| 160 | +When configuring additional policies the following execution positions can be specified: |
| 161 | + |
| 162 | +- `perCall` - When a policy is assigned a `perCall` position, it will be run once before sending the request. If the request needs to be retried, the policy will not be called again. |
| 163 | +- `perRetry` - When a policy is assigned a `perRetry` position, the policy will be executed again before each new request is sent. |
| 164 | + |
| 165 | +```typescript |
| 166 | +type SendRequest = (request: PipelineRequest) => Promise<PipelineResponse>; |
| 167 | + |
| 168 | +const sendRequest = (request: PipelineRequest, next: SendRequest) => next(request); |
| 169 | + |
| 170 | +const policy1: PipelinePolicy = { |
| 171 | + name: "policy1", |
| 172 | + sendRequest, |
| 173 | +}; |
| 174 | +const policy2: PipelinePolicy = { |
| 175 | + name: "policy2", |
| 176 | + sendRequest, |
| 177 | +}; |
| 178 | + |
| 179 | +const client = new ServiceClient({ |
| 180 | + additionalPolicies: [ |
| 181 | + { |
| 182 | + policy: policy1, |
| 183 | + /** |
| 184 | + * Determines if this policy be applied before or after retry logic. |
| 185 | + * Only use `perRetry` if you need to modify the request again |
| 186 | + * each time the operation is retried due to retryable service |
| 187 | + * issues. |
| 188 | + */ |
| 189 | + position: "perRetry", |
| 190 | + }, |
| 191 | + { policy: policy2, position: "perCall" }, |
| 192 | + ], |
| 193 | +}); |
| 194 | +``` |
| 195 | + |
| 196 | +# HttpClient |
| 197 | + |
| 198 | +The `HttpClient` is the last logical piece of the pipeline. The `HttpClient` will send the request over the wire after all policies have run and will hand off the response back to the pipeline once the server responds. There are different default implementations for the `HttpClient` based on if the SDK is running inside a Node or browser runtime. |
| 199 | + |
| 200 | +For browsers, an `HttpClient` implementation based on `Fetch API` will be used. When running in Node, an `HttpClient` based on Node's native `https` module will be used. |
| 201 | + |
| 202 | +A custom `HttpClient` implementation can be provided when constructing an SDK client. |
| 203 | + |
| 204 | +# Examples |
| 205 | + |
| 206 | +## Inserting a new policy at a specific place |
| 207 | + |
| 208 | +```typescript |
| 209 | +interface TracingPolicyOptions { |
| 210 | + tracingEnabled: boolean; |
| 211 | +} |
| 212 | + |
| 213 | +function tracingPolicy(options: TracingPolicyOptions): PipelinePolicy { |
| 214 | + return { |
| 215 | + name: "TracingPolicy", |
| 216 | + sendRequest: (request, next) => { |
| 217 | + if (options.tracingEnabled) { |
| 218 | + // add some headers |
| 219 | + request.headers.set("tracing-id", "123456"); |
| 220 | + } |
| 221 | + |
| 222 | + return next(request); |
| 223 | + }, |
| 224 | + }; |
| 225 | +} |
| 226 | + |
| 227 | +// Extend an existing Pipeline |
| 228 | +pipeline.addPolicy(tracingPolicy({ tracingEnabled: true }), { beforePhase: "Retry" }); |
| 229 | +``` |
| 230 | + |
| 231 | +## Removing policies |
| 232 | + |
| 233 | +Policies can be removed from a pipeline, consumers can target a specifc policy by name or remove all the policies in a specific phase |
| 234 | + |
| 235 | +```typescript |
| 236 | +// Remove a single policy, referencing it by name |
| 237 | +pipeline.removePolicy({ name: "customPolicy" }); |
| 238 | + |
| 239 | +// Remove all policies from a phase |
| 240 | +pipeline.removePolicy({ phase: "Retry" }); |
| 241 | +``` |
| 242 | + |
| 243 | +## List policies |
| 244 | + |
| 245 | +For debugging purposes, consumers can list all the policies in a pipeline calling `getOrderedPolicies()`, which returns the current set of policies in the pipeline in the order in which they will be applied to the request, remember that policies have a bubbling nature, this means that the **first** policy to touch the request will be the **last** policy to work on the response. |
| 246 | + |
| 247 | +```typescript |
| 248 | +const policies = pipeline.getOrderedPolicies(); |
| 249 | + |
| 250 | +for (const policy of policies) { |
| 251 | + console.log(policy.name); |
| 252 | +} |
| 253 | +``` |
0 commit comments