diff --git a/abstractions/typescript/src/apiError.ts b/abstractions/typescript/src/apiError.ts new file mode 100644 index 0000000000..d0389c2a09 --- /dev/null +++ b/abstractions/typescript/src/apiError.ts @@ -0,0 +1,11 @@ +/** Parent interface for errors thrown by the client when receiving failed responses to its requests. */ +interface ApiError extends Error { +} + +interface ApiErrorConstructor extends ErrorConstructor { + new(message?: string): ApiError; + (message?: string): ApiError; + readonly prototype: ApiError; +} + +export var ApiError: ApiErrorConstructor; \ No newline at end of file diff --git a/abstractions/typescript/src/index.ts b/abstractions/typescript/src/index.ts index 1dc1256025..e387655ad9 100644 --- a/abstractions/typescript/src/index.ts +++ b/abstractions/typescript/src/index.ts @@ -1,4 +1,5 @@ export * from './apiClientBuilder'; +export * from './apiError'; export * from './authentication'; export * from './dateOnly'; export * from './duration'; diff --git a/abstractions/typescript/src/nativeResponseHandler.ts b/abstractions/typescript/src/nativeResponseHandler.ts index 292ecf0fcd..6be9a1498d 100644 --- a/abstractions/typescript/src/nativeResponseHandler.ts +++ b/abstractions/typescript/src/nativeResponseHandler.ts @@ -1,11 +1,15 @@ import { ResponseHandler } from "./responseHandler"; +import { Parsable } from "./serialization"; /** Default response handler to access the native response object. */ export class NativeResponseHandler implements ResponseHandler { /** Native response object as returned by the core service */ public value?: any; - public handleResponseAsync(response: NativeResponseType): Promise { + /** The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. */ + public errorMappings: Record Parsable> | undefined + public handleResponseAsync(response: NativeResponseType, errorMappings: Record Parsable> | undefined): Promise { this.value = response; + this.errorMappings = errorMappings; return Promise.resolve(undefined as any); } } \ No newline at end of file diff --git a/abstractions/typescript/src/requestAdapter.ts b/abstractions/typescript/src/requestAdapter.ts index 0a02ebaa07..48a175c04c 100644 --- a/abstractions/typescript/src/requestAdapter.ts +++ b/abstractions/typescript/src/requestAdapter.ts @@ -14,46 +14,51 @@ export interface RequestAdapter { * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param type the class of the response model to deserialize the response into. * @typeParam ModelType the type of the response model to deserialize the response into. * @return a {@link Promise} with the deserialized response model. */ - sendAsync(requestInfo: RequestInformation, type: new() => ModelType, responseHandler: ResponseHandler | undefined): Promise; + sendAsync(requestInfo: RequestInformation, type: new() => ModelType, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise; /** * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param type the class of the response model to deserialize the response into. * @typeParam ModelType the type of the response model to deserialize the response into. * @return a {@link Promise} with the deserialized response model collection. */ - sendCollectionAsync(requestInfo: RequestInformation, type: new() => ModelType, responseHandler: ResponseHandler | undefined): Promise; + sendCollectionAsync(requestInfo: RequestInformation, type: new() => ModelType, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise; /** * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. * @param requestInfo the request info to execute. * @param responseType the class of the response model to deserialize the response into. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param type the class of the response model to deserialize the response into. * @typeParam ResponseType the type of the response model to deserialize the response into. * @return a {@link Promise} with the deserialized response model collection. */ - sendCollectionOfPrimitiveAsync(requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date", responseHandler: ResponseHandler | undefined): Promise; + sendCollectionOfPrimitiveAsync(requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date", responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise; /** * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param responseType the class of the response model to deserialize the response into. * @typeParam ResponseType the type of the response model to deserialize the response into. * @return a {@link Promise} with the deserialized primitive response model. */ - sendPrimitiveAsync(requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date" | "ArrayBuffer", responseHandler: ResponseHandler | undefined): Promise; + sendPrimitiveAsync(requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date" | "ArrayBuffer", responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise; /** * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @return a {@link Promise} of void. */ - sendNoResponseContentAsync(requestInfo: RequestInformation, responseHandler: ResponseHandler | undefined): Promise; + sendNoResponseContentAsync(requestInfo: RequestInformation, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise; /** * Enables the backing store proxies for the SerializationWriters and ParseNodes in use. * @param backingStoreFactory the backing store factory to use. diff --git a/abstractions/typescript/src/responseHandler.ts b/abstractions/typescript/src/responseHandler.ts index 50fbd0a50f..3efbed0de6 100644 --- a/abstractions/typescript/src/responseHandler.ts +++ b/abstractions/typescript/src/responseHandler.ts @@ -1,11 +1,14 @@ +import { Parsable } from "./serialization"; + /** Defines the contract for a response handler. */ export interface ResponseHandler { /** * Callback method that is invoked when a response is received. * @param response The native response object. + * @param errorMappings the error factories mapping to use in case of a failed request. * @typeParam NativeResponseType The type of the native response object. * @typeParam ModelType The type of the response model object. * @return A {@link Promise} that represents the asynchronous operation and contains the deserialized response. */ - handleResponseAsync(response: NativeResponseType): Promise; + handleResponseAsync(response: NativeResponseType, errorMappings: Record Parsable> | undefined): Promise; } \ No newline at end of file diff --git a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs index f0d49fab1d..f648c90846 100644 --- a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs +++ b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs @@ -205,12 +205,12 @@ private async Task ThrowFailedResponse(HttpResponseMessage response, Dictionary< !errorMapping.TryGetValue(statusCodeAsString, out errorFactory) && !(statusCodeAsInt >= 400 && statusCodeAsInt < 500 && errorMapping.TryGetValue("4XX", out errorFactory)) && !(statusCodeAsInt >= 500 && statusCodeAsInt < 600 && errorMapping.TryGetValue("5XX", out errorFactory))) - throw new HttpRequestException($"The server returned an unexpected status code and no error factory is registered for this code: {statusCodeAsString}"); + throw new ApiException($"The server returned an unexpected status code and no error factory is registered for this code: {statusCodeAsString}"); var rootNode = await GetRootParseNode(response); var result = rootNode.GetErrorValue(errorFactory); if(result is not Exception ex) - throw new HttpRequestException($"The server returned an unexpected status code and the error registered for this code failed to deserialize: {statusCodeAsString}"); + throw new ApiException($"The server returned an unexpected status code and the error registered for this code failed to deserialize: {statusCodeAsString}"); else throw ex; } private async Task GetRootParseNode(HttpResponseMessage response) diff --git a/http/typescript/fetch/src/fetchRequestAdapter.ts b/http/typescript/fetch/src/fetchRequestAdapter.ts index 1fb8ecec95..40f639affe 100644 --- a/http/typescript/fetch/src/fetchRequestAdapter.ts +++ b/http/typescript/fetch/src/fetchRequestAdapter.ts @@ -1,4 +1,4 @@ -import { AuthenticationProvider, BackingStoreFactory, BackingStoreFactorySingleton, RequestAdapter, Parsable, ParseNodeFactory, RequestInformation, ResponseHandler, ParseNodeFactoryRegistry, enableBackingStoreForParseNodeFactory, SerializationWriterFactoryRegistry, enableBackingStoreForSerializationWriterFactory, SerializationWriterFactory } from '@microsoft/kiota-abstractions'; +import { ApiError, AuthenticationProvider, BackingStoreFactory, BackingStoreFactorySingleton, RequestAdapter, Parsable, ParseNodeFactory, RequestInformation, ResponseHandler, ParseNodeFactoryRegistry, enableBackingStoreForParseNodeFactory, SerializationWriterFactoryRegistry, enableBackingStoreForSerializationWriterFactory, SerializationWriterFactory, ParseNode } from '@microsoft/kiota-abstractions'; import { HttpClient } from './httpClient'; export class FetchRequestAdapter implements RequestAdapter { @@ -35,25 +35,21 @@ export class FetchRequestAdapter implements RequestAdapter { if (segments.length === 0) return undefined; else return segments[0]; } - public sendCollectionOfPrimitiveAsync = async (requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date", responseHandler: ResponseHandler | undefined): Promise => { + public sendCollectionOfPrimitiveAsync = async (requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date", responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } else { + await this.throwFailedResponses(response, errorMappings); switch (responseType) { case 'string': case 'number': case 'boolean': case 'Date': - const payload = await response.arrayBuffer(); - const responseContentType = this.getResponseContentType(response); - if (!responseContentType) - throw new Error("no response content type found for deserialization"); - - const rootNode = this.parseNodeFactory.getRootParseNode(responseContentType, payload); + const rootNode = await this.getRootParseNode(response); if (responseType === 'string') { return rootNode.getCollectionOfPrimitiveValues() as unknown as ResponseType[]; } else if (responseType === 'number') { @@ -68,50 +64,43 @@ export class FetchRequestAdapter implements RequestAdapter { } } } - public sendCollectionAsync = async (requestInfo: RequestInformation, type: new () => ModelType, responseHandler: ResponseHandler | undefined): Promise => { + public sendCollectionAsync = async (requestInfo: RequestInformation, type: new () => ModelType, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } else { - const payload = await response.arrayBuffer(); - const responseContentType = this.getResponseContentType(response); - if (!responseContentType) - throw new Error("no response content type found for deserialization"); - - const rootNode = this.parseNodeFactory.getRootParseNode(responseContentType, payload); + await this.throwFailedResponses(response, errorMappings); + const rootNode = await this.getRootParseNode(response); const result = rootNode.getCollectionOfObjectValues(type); return result as unknown as ModelType[]; } } - public sendAsync = async (requestInfo: RequestInformation, type: new () => ModelType, responseHandler: ResponseHandler | undefined): Promise => { + public sendAsync = async (requestInfo: RequestInformation, type: new () => ModelType, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } else { - const payload = await response.arrayBuffer(); - const responseContentType = this.getResponseContentType(response); - if (!responseContentType) - throw new Error("no response content type found for deserialization"); - - const rootNode = this.parseNodeFactory.getRootParseNode(responseContentType, payload); + await this.throwFailedResponses(response, errorMappings); + const rootNode = await this.getRootParseNode(response); const result = rootNode.getObjectValue(type); return result as unknown as ModelType; } } - public sendPrimitiveAsync = async (requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date" | "ArrayBuffer", responseHandler: ResponseHandler | undefined): Promise => { + public sendPrimitiveAsync = async (requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date" | "ArrayBuffer", responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } else { + await this.throwFailedResponses(response, errorMappings); switch (responseType) { case "ArrayBuffer": return await response.arrayBuffer() as unknown as ResponseType; @@ -119,12 +108,7 @@ export class FetchRequestAdapter implements RequestAdapter { case 'number': case 'boolean': case 'Date': - const payload = await response.arrayBuffer(); - const responseContentType = this.getResponseContentType(response); - if (!responseContentType) - throw new Error("no response content type found for deserialization"); - - const rootNode = this.parseNodeFactory.getRootParseNode(responseContentType, payload); + const rootNode = await this.getRootParseNode(response); if (responseType === 'string') { return rootNode.getStringValue() as unknown as ResponseType; } else if (responseType === 'number') { @@ -139,14 +123,15 @@ export class FetchRequestAdapter implements RequestAdapter { } } } - public sendNoResponseContentAsync = async (requestInfo: RequestInformation, responseHandler: ResponseHandler | undefined): Promise => { + public sendNoResponseContentAsync = async (requestInfo: RequestInformation, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } + await this.throwFailedResponses(response, errorMappings); } public enableBackingStore = (backingStoreFactory?: BackingStoreFactory | undefined): void => { this.parseNodeFactory = enableBackingStoreForParseNodeFactory(this.parseNodeFactory); @@ -157,6 +142,35 @@ export class FetchRequestAdapter implements RequestAdapter { BackingStoreFactorySingleton.instance = backingStoreFactory; } } + private getRootParseNode = async (response: Response) : Promise => { + const payload = await response.arrayBuffer(); + const responseContentType = this.getResponseContentType(response); + if (!responseContentType) + throw new Error("no response content type found for deserialization"); + + return this.parseNodeFactory.getRootParseNode(responseContentType, payload); + } + private throwFailedResponses = async (response: Response, errorMappings: Record Parsable> | undefined): Promise => { + if(response.ok) return; + + const statusCode = response.status; + const statusCodeAsString = statusCode.toString(); + if(!errorMappings || + !errorMappings[statusCodeAsString] && + !(statusCode >= 400 && statusCode < 500 && errorMappings['4XX']) && + !(statusCode >= 500 && statusCode < 600 && errorMappings['5XX'])) + throw new ApiError("the server returned an unexpected status code and no error class is registered for this code " + statusCode); + + const factory = errorMappings[statusCodeAsString] ?? + (statusCode >= 400 && statusCode < 500 ? errorMappings['4XX'] : undefined) ?? + (statusCode >= 500 && statusCode < 600 ? errorMappings['5XX'] : undefined); + + const rootNode = await this.getRootParseNode(response); + const error = rootNode.getObjectValue(factory); + + if(error instanceof Error) throw error; + else throw new ApiError("unexpected error type" + typeof(error)) + } private getHttpResponseMessage = async (requestInfo: RequestInformation): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null');