@@ -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