/// <summary> /// Called when <c>error</c> node presents in the API response. /// </summary> /// <param name="errorCode">Error code. (<c>error.code</c>)</param> /// <param name="errorMessage">Errir message. (<c>error.info</c>)</param> /// <param name="errorNode">The <c>error</c> JSON node.</param> /// <param name="responseNode">The JSON root of the API response.</param> /// <param name="context">The response parsing context, used for initiating a retry.</param> /// <remarks> /// <para>The default implementation for this method throws a <see cref="OperationFailedException"/> /// or one of its derived exceptions. The default exception mapping is as follows</para> /// <list type="table"> /// <listheader> /// <term><paramref name="errorCode"/> value</term> /// <description>Mapped exception type</description> /// </listheader> /// <item> /// <term><c>permissiondenied</c>, <c>readapidenied</c>, <c>mustbeloggedin</c></term> /// <description><see cref="UnauthorizedOperationException"/></description> /// </item> /// <item> /// <term><c>permissions</c> (Flow)</term> /// <description><see cref="UnauthorizedOperationException"/></description> /// </item> /// <item> /// <term><c>badtoken</c></term> /// <description><see cref="BadTokenException"/></description> /// </item> /// <item> /// <term><c>unknown_action</c></term> /// <description><see cref="InvalidActionException"/></description> /// </item> /// <item> /// <term><c>assertuserfailed</c>, <c>assertbotfailed</c></term> /// <description><see cref="AccountAssertionFailureException"/></description> /// </item> /// <item> /// <term><c>*conflict</c></term> /// <description><see cref="OperationConflictException"/></description> /// </item> /// <item> /// <term><c>prev_revision</c> (Flow)</term> /// <description><see cref="OperationConflictException"/></description> /// </item> /// <item> /// <term>others</term> /// <description><see cref="OperationFailedException"/></description> /// </item> /// </list> /// </remarks> protected virtual void OnApiError(string errorCode, string errorMessage, JToken errorNode, JToken responseNode, WikiResponseParsingContext context) { switch (errorCode) { case "permissiondenied": case "readapidenied": // You need read permission to use this module case "mustbeloggedin": // You must be logged in to upload this file. throw new UnauthorizedOperationException(errorCode, errorMessage); case "permissions": if (errorNode["permissions"] != null && errorNode["permissions"].Type != JTokenType.Null) { errorMessage += " Desired permissions:" + errorNode["permissions"]?.ToString(Formatting.None); } throw new UnauthorizedOperationException(errorCode, errorMessage); case "badtoken": throw new BadTokenException(errorCode, errorMessage); case "unknown_action": throw new InvalidActionException(errorCode, errorMessage); case "assertuserfailed": case "assertbotfailed": throw new AccountAssertionFailureException(errorCode, errorMessage); case "prev_revision": throw new OperationConflictException(errorCode, errorMessage); default: if (errorCode.EndsWith("conflict")) { throw new OperationConflictException(errorCode, errorMessage); } // "messages": [ // { // "name": "wikibase-api-failed-save", // "parameters": [], // "html": { // "*": "The save has failed." // } // }, // ...] var messages = (JArray)errorNode["messages"]; if (messages != null && messages.Count > 1 && messages[0]["html"] != null) { errorMessage = string.Join(" ", messages.Select(m => (string)m["html"]["*"])); } throw new OperationFailedException(errorCode, errorMessage); } }
/// <inheritdoc /> async Task <object> IWikiResponseMessageParser <T> .ParseResponseAsync(HttpResponseMessage response, WikiResponseParsingContext context) { return(await ParseResponseAsync(response, context)); }
/// <summary> /// Parses the specified HTTP response message. /// </summary> /// <param name="response">The HTTP response message to parse.</param> /// <param name="context">The parsing context.</param> /// <returns>A strongly-typed object containing the desired response.</returns> /// <remarks>For general guidance on how this method should be implemented, see <see cref="IWikiResponseMessageParser{T}.ParseResponseAsync"/>.</remarks> public abstract Task <T> ParseResponseAsync(HttpResponseMessage response, WikiResponseParsingContext context);
/// <inheritdoc /> /// <remarks> /// <para>This method checks the HTTP status code first. /// For non-successful HTTP status codes, this method will request for a retry.</para> /// <para>Then the content will be parsed as JSON, in <see cref="JToken"/>. If there is /// <see cref="JsonException"/> thrown while parsing the response, a retry will be requested.</para> /// <para>Finally, before returning the parsed JSON, this method checks for <c>warning</c> and <c>error</c> /// nodes. If there exists <c>warning</c> node, a warning will be issued to the logger. If there exists <c>error</c> /// node, a <see cref="OperationFailedException"/> or its derived exception will be thrown. You can /// customize the error generation behavior by overriding <see cref="OnApiError"/> method.</para> /// </remarks> public override async Task <JToken> ParseResponseAsync(HttpResponseMessage response, WikiResponseParsingContext context) { if (response == null) { throw new ArgumentNullException(nameof(response)); } if (context == null) { throw new ArgumentNullException(nameof(context)); } // Check response status first. if (!response.IsSuccessStatusCode) { context.NeedRetry = true; response.EnsureSuccessStatusCode(); } JToken jroot; try { await using var s = await response.Content.ReadAsStreamAsync(); jroot = await MediaWikiHelper.ParseJsonAsync(s, context.CancellationToken); } catch (JsonException) { // Input is not valid json. context.NeedRetry = true; throw; } // See https://www.mediawiki.org/wiki/API:Errors_and_warnings . /* * "warnings": { * "main": { * "*": "xxxx" * }, * "login": { * "*": "xxxx" * } * } */ // Note that in MW 1.19, action=logout returns [] instead of {} if (jroot is JObject jobj) { if (jobj["warnings"] != null && context.Logger.IsEnabled(LogLevel.Warning)) { foreach (var module in ((JObject)jobj["warnings"]).Properties()) { context.Logger.LogWarning("API warning [{Module}]: {Warning}", module.Name, module.Value); } } var err = jobj["error"]; if (err != null) { var errcode = (string)err["code"]; // err["*"]: API usage. var errmessage = ((string)err["info"]).Trim(); context.Logger.LogWarning("API error: {Code} - {Message}", errcode, errmessage); OnApiError(errcode, errmessage, err, jobj, context); } } return(jroot); }
/// <summary> /// Called when <c>error</c> node presents in the API response. /// </summary> /// <param name="errorCode">Error code. (<c>error.code</c>)</param> /// <param name="errorMessage">Error message. (<c>error.info</c>)</param> /// <param name="errorNode">The <c>error</c> JSON node.</param> /// <param name="responseNode">The JSON root of the API response.</param> /// <param name="context">The response parsing context, used for initiating a retry.</param> /// <remarks> /// <para>The default implementation for this method throws a <see cref="OperationFailedException"/> /// or one of its derived exceptions. The default exception mapping is as follows</para> /// <list type="table"> /// <listheader> /// <term><paramref name="errorCode"/> value</term> /// <description>Mapped exception type</description> /// </listheader> /// <item> /// <term><c>maxlag</c></term> /// <description><see cref="ServerLagException"/>; <see cref="WikiResponseParsingContext.NeedRetry"/> will be set to <c>true</c>.</description> /// </item> /// <item> /// <term><c>permissiondenied</c>, <c>readapidenied</c>, <c>mustbeloggedin</c></term> /// <description><see cref="UnauthorizedOperationException"/></description> /// </item> /// <item> /// <term><c>permissions</c> (Flow)</term> /// <description><see cref="UnauthorizedOperationException"/></description> /// </item> /// <item> /// <term><c>badtoken</c></term> /// <description><see cref="BadTokenException"/></description> /// </item> /// <item> /// <term><c>unknown_action</c></term> /// <description><see cref="InvalidActionException"/></description> /// </item> /// <item> /// <term><c>assertuserfailed</c>, <c>assertbotfailed</c></term> /// <description><see cref="AccountAssertionFailureException"/></description> /// </item> /// <item> /// <term><c>*conflict</c></term> /// <description><see cref="OperationConflictException"/></description> /// </item> /// <item> /// <term><c>prev_revision</c> (Flow)</term> /// <description><see cref="OperationConflictException"/></description> /// </item> /// <item> /// <item> /// <term><c>internal_api_error*</c></term> /// <description><see cref="MediaWikiRemoteException"/></description> /// </item> /// <term>others</term> /// <description><see cref="OperationFailedException"/></description> /// </item> /// </list> /// </remarks> protected virtual void OnApiError(string errorCode, string errorMessage, JToken errorNode, JToken responseNode, WikiResponseParsingContext context) { var fullMessage = errorMessage; // Append additional messages from WMF, if any. var jmessages = (JArray)errorNode["messages"]; if (jmessages != null && jmessages.Count > 1 && jmessages[0]["html"] != null) { // jmessages[0] usually is the same as errorMessage fullMessage = string.Join(" ", jmessages.Select(m => (string)m["html"]["*"])); } switch (errorCode) { case "maxlag": // maxlag reached. context.NeedRetry = true; throw new ServerLagException(errorCode, fullMessage, (double?)errorNode["lag"] ?? 0, (string)errorNode["type"], (string)errorNode["host"]); case "permissiondenied": case "readapidenied": // You need read permission to use this module. case "mustbeloggedin": // You must be logged in to upload this file. throw new UnauthorizedOperationException(errorCode, fullMessage); case "permissions": JToken jPermissions; if ((jPermissions = errorNode["permissions"]) != null) { if (jPermissions.Type != JTokenType.Null) { var permissions = jPermissions is JArray a ? a.Select(c => c is JValue v?Convert.ToString(v.Value, CultureInfo.InvariantCulture) : c.ToString()) : new[] { jPermissions is JValue v1 ? Convert.ToString(v1.Value, CultureInfo.InvariantCulture) : jPermissions.ToString() }; throw new UnauthorizedOperationException(errorCode, fullMessage, permissions.ToList() !); } } throw new UnauthorizedOperationException(errorCode, fullMessage); case "badtoken": throw new BadTokenException(errorCode, fullMessage); case "unknown_action": throw new InvalidActionException(errorCode, fullMessage); case "assertuserfailed": case "assertbotfailed": throw new AccountAssertionFailureException(errorCode, fullMessage); case "prev_revision": throw new OperationConflictException(errorCode, fullMessage); case "badvalue": // since 1.35.0-wmf.19 // throw more specific Exception, if possible. if (fullMessage.Contains("\"action\"")) { throw new InvalidActionException(errorCode, fullMessage); } throw new BadValueException(errorCode, fullMessage); default: if (errorCode.StartsWith("unknown_", StringComparison.OrdinalIgnoreCase)) { throw new BadValueException(errorCode, fullMessage); } if (errorCode.EndsWith("conflict", StringComparison.OrdinalIgnoreCase)) { throw new OperationConflictException(errorCode, fullMessage); } // "messages": [ // { // "name": "wikibase-api-failed-save", // "parameters": [], // "html": { // "*": "The save has failed." // } // }, // ...] if (errorCode.StartsWith("internal_api_error", StringComparison.OrdinalIgnoreCase)) { throw new MediaWikiRemoteException(errorCode, fullMessage, (string)errorNode["errorclass"], (string)errorNode["*"]); } throw new OperationFailedException(errorCode, fullMessage); } }