Skip to content

Commit 4d49f15

Browse files
authored
feat: retry on unauthorized 401 (#413)
Fixes #157.
1 parent 72dec7b commit 4d49f15

File tree

2 files changed

+335
-76
lines changed

2 files changed

+335
-76
lines changed

src/FMData.Rest/FileMakerRestClient.cs

Lines changed: 108 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class FileMakerRestClient : FileMakerApiClientBase, IFileMakerRestClient
4646

4747
#region FM DATA SPECIFIC
4848
private readonly int _tokenExpiration = 15;
49+
private readonly int _maxAuthRetries = 1;
4950
private string _dataToken;
5051
private AuthenticationHeaderValue _authHeader;
5152
private DateTime _dataTokenLastUse = DateTime.MinValue;
@@ -666,13 +667,13 @@ public override async Task<string> RunScriptAsync(string layout, string script,
666667
{
667668
uri += $"?script.param={Uri.EscapeDataString(scriptParameter)}";
668669
}
669-
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
670670

671-
// include auth token
672-
requestMessage.Headers.Authorization = _authHeader;
673-
674-
// run the patch action
675-
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
671+
var response = await RetryOnUnauthorizedAsync(async () =>
672+
{
673+
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
674+
requestMessage.Headers.Authorization = _authHeader;
675+
return await Client.SendAsync(requestMessage).ConfigureAwait(false);
676+
}).ConfigureAwait(false);
676677

677678
if (response.StatusCode == HttpStatusCode.NotFound)
678679
{
@@ -734,28 +735,9 @@ public async Task<HttpResponseMessage> ExecuteRequestAsync(
734735
// we're about to use the token so update date used, and refresh if needed.
735736
await UpdateTokenDateAsync().ConfigureAwait(false);
736737

737-
var str = req.SerializeRequest();
738-
var httpContent = new StringContent(str, Encoding.UTF8, "application/json");
739-
740-
// do not pass character set.
741-
// this is due to fms 18 returning Bad Request when specified
742-
// this hack is backward compatible for FMS17
743-
httpContent.Headers.ContentType.CharSet = null;
744-
745-
var httpRequest = new HttpRequestMessage(method, requestUri);
746-
747-
// don't include body content on requests for http get
748-
if (method != HttpMethod.Get)
749-
{
750-
httpRequest.Content = httpContent;
751-
}
752-
753-
// include our authorization header
754-
httpRequest.Headers.Authorization = _authHeader;
755-
756-
// run and return the action
757-
var response = await Client.SendAsync(httpRequest).ConfigureAwait(false);
758-
return response;
738+
return await RetryOnUnauthorizedAsync(
739+
async () => await SendDataApiRequestAsync(method, requestUri, req).ConfigureAwait(false)
740+
).ConfigureAwait(false);
759741
}
760742

761743
/// <summary>
@@ -812,21 +794,19 @@ public override async Task<IResponse> SetGlobalFieldAsync(string baseTable, stri
812794

813795
var method = new HttpMethod("PATCH");
814796

815-
var requestMessage = new HttpRequestMessage(method, $"{BaseEndPoint}/globals")
797+
var response = await RetryOnUnauthorizedAsync(async () =>
816798
{
817-
Content = new StringContent(json, Encoding.UTF8, "application/json")
818-
};
819-
820-
// include auth token
821-
requestMessage.Headers.Authorization = _authHeader;
822-
823-
// do not pass character set.
824-
// this is due to fms 18 returning Bad Request when specified
825-
// this hack is backward compatible for FMS17
826-
requestMessage.Content.Headers.ContentType.CharSet = null;
827-
828-
// run the patch action
829-
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
799+
var requestMessage = new HttpRequestMessage(method, $"{BaseEndPoint}/globals")
800+
{
801+
Content = new StringContent(json, Encoding.UTF8, "application/json")
802+
};
803+
requestMessage.Headers.Authorization = _authHeader;
804+
// do not pass character set.
805+
// this is due to fms 18 returning Bad Request when specified
806+
// this hack is backward compatible for FMS17
807+
requestMessage.Content.Headers.ContentType.CharSet = null;
808+
return await Client.SendAsync(requestMessage).ConfigureAwait(false);
809+
}).ConfigureAwait(false);
830810

831811
if (response.StatusCode == HttpStatusCode.NotFound)
832812
{
@@ -926,13 +906,13 @@ public override async Task<IReadOnlyCollection<LayoutListItem>> GetLayoutsAsync(
926906
// generate request url
927907
var uri = $"{FmsUri}/fmi/data/{_targetVersion}/"
928908
+ $"databases/{Uri.EscapeDataString(FileName)}/layouts";
929-
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
930-
931-
// include auth token
932-
requestMessage.Headers.Authorization = _authHeader;
933909

934-
// run the patch action
935-
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
910+
var response = await RetryOnUnauthorizedAsync(async () =>
911+
{
912+
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
913+
requestMessage.Headers.Authorization = _authHeader;
914+
return await Client.SendAsync(requestMessage).ConfigureAwait(false);
915+
}).ConfigureAwait(false);
936916

937917
if (response.StatusCode == HttpStatusCode.NotFound)
938918
{
@@ -965,13 +945,13 @@ public override async Task<IReadOnlyCollection<ScriptListItem>> GetScriptsAsync(
965945
// generate request url
966946
var uri = $"{FmsUri}/fmi/data/{_targetVersion}"
967947
+ $"/databases/{Uri.EscapeDataString(FileName)}/scripts";
968-
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
969-
970-
// include auth token
971-
requestMessage.Headers.Authorization = _authHeader;
972948

973-
// run the patch action
974-
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
949+
var response = await RetryOnUnauthorizedAsync(async () =>
950+
{
951+
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
952+
requestMessage.Headers.Authorization = _authHeader;
953+
return await Client.SendAsync(requestMessage).ConfigureAwait(false);
954+
}).ConfigureAwait(false);
975955

976956
if (response.StatusCode == HttpStatusCode.NotFound)
977957
{
@@ -1011,13 +991,13 @@ public override async Task<LayoutMetadata> GetLayoutAsync(string layout, int? re
1011991
{
1012992
uri += $"?recordId={recordId}";
1013993
}
1014-
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
1015994

1016-
// include auth token
1017-
requestMessage.Headers.Authorization = _authHeader;
1018-
1019-
// run the patch action
1020-
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
995+
var response = await RetryOnUnauthorizedAsync(async () =>
996+
{
997+
var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
998+
requestMessage.Headers.Authorization = _authHeader;
999+
return await Client.SendAsync(requestMessage).ConfigureAwait(false);
1000+
}).ConfigureAwait(false);
10211001

10221002
if (response.StatusCode == HttpStatusCode.NotFound)
10231003
{
@@ -1061,26 +1041,22 @@ public override async Task<IEditResponse> UpdateContainerAsync(
10611041
{
10621042
await UpdateTokenDateAsync().ConfigureAwait(false); // about to use token, so update
10631043

1064-
var form = new MultipartFormDataContent();
1065-
1066-
//var stream = new MemoryStream(content);
1067-
//var streamContent = new StreamContent(stream);
10681044
var uri = ContainerEndpoint(layout, recordId, fieldName, repetition);
10691045

1070-
var containerContent = new ByteArrayContent(content);
1071-
containerContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data");
1072-
1073-
form.Add(containerContent, "upload", Path.GetFileName(fileName));
1074-
1075-
var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri)
1046+
var response = await RetryOnUnauthorizedAsync(async () =>
10761047
{
1077-
Content = form
1078-
};
1079-
1080-
// include auth token
1081-
requestMessage.Headers.Authorization = _authHeader;
1048+
var form = new MultipartFormDataContent();
1049+
var containerContent = new ByteArrayContent(content);
1050+
containerContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data");
1051+
form.Add(containerContent, "upload", Path.GetFileName(fileName));
10821052

1083-
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
1053+
var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri)
1054+
{
1055+
Content = form
1056+
};
1057+
requestMessage.Headers.Authorization = _authHeader;
1058+
return await Client.SendAsync(requestMessage).ConfigureAwait(false);
1059+
}).ConfigureAwait(false);
10841060

10851061
if (response.StatusCode == HttpStatusCode.NotFound)
10861062
{
@@ -1132,6 +1108,62 @@ protected override async Task<byte[]> GetContainerOnClient(string containerEndPo
11321108
}
11331109

11341110
#region Private Helpers and utility methods
1111+
/// <summary>
1112+
/// Invalidates the current token so the next authentication check triggers a refresh.
1113+
/// </summary>
1114+
private void InvalidateToken()
1115+
{
1116+
_dataToken = null;
1117+
_authHeader = null;
1118+
}
1119+
1120+
/// <summary>
1121+
/// Serializes an <see cref="IFileMakerRequest"/> to JSON, builds a fresh <see cref="HttpRequestMessage"/>,
1122+
/// and sends it to the Data API with the current auth header.
1123+
/// </summary>
1124+
private async Task<HttpResponseMessage> SendDataApiRequestAsync(HttpMethod method, string requestUri, IFileMakerRequest req)
1125+
{
1126+
var str = req.SerializeRequest();
1127+
var httpContent = new StringContent(str, Encoding.UTF8, "application/json");
1128+
1129+
// do not pass character set.
1130+
// this is due to fms 18 returning Bad Request when specified
1131+
// this hack is backward compatible for FMS17
1132+
httpContent.Headers.ContentType.CharSet = null;
1133+
1134+
var httpRequest = new HttpRequestMessage(method, requestUri);
1135+
1136+
// don't include body content on requests for http get
1137+
if (method != HttpMethod.Get)
1138+
{
1139+
httpRequest.Content = httpContent;
1140+
}
1141+
1142+
// include our authorization header
1143+
httpRequest.Headers.Authorization = _authHeader;
1144+
1145+
// run and return the action
1146+
return await Client.SendAsync(httpRequest).ConfigureAwait(false);
1147+
}
1148+
1149+
/// <summary>
1150+
/// Sends a request and retries up to <see cref="_maxAuthRetries"/> times on 401 Unauthorized,
1151+
/// refreshing the auth token before each retry.
1152+
/// </summary>
1153+
private async Task<HttpResponseMessage> RetryOnUnauthorizedAsync(Func<Task<HttpResponseMessage>> sendRequest)
1154+
{
1155+
var response = await sendRequest().ConfigureAwait(false);
1156+
var retries = 0;
1157+
while (response.StatusCode == HttpStatusCode.Unauthorized && retries < _maxAuthRetries)
1158+
{
1159+
retries++;
1160+
InvalidateToken();
1161+
await RefreshTokenAsync().ConfigureAwait(false);
1162+
response = await sendRequest().ConfigureAwait(false);
1163+
}
1164+
return response;
1165+
}
1166+
11351167
/// <summary>
11361168
/// Converts a JToken instance and maps it to the generic type.
11371169
/// </summary>

0 commit comments

Comments
 (0)