react-native-fast-image 8.17.0 → 8.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,9 @@ import androidx.annotation.Nullable;
7
7
  import com.bumptech.glide.Glide;
8
8
  import com.bumptech.glide.Registry;
9
9
  import com.bumptech.glide.annotation.GlideModule;
10
+ import com.bumptech.glide.load.ImageHeaderParser;
11
+ import com.bumptech.glide.load.ImageHeaderParserUtils;
12
+ import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
10
13
  import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
11
14
  import com.bumptech.glide.load.Options;
12
15
  import com.bumptech.glide.load.model.GlideUrl;
@@ -21,6 +24,7 @@ import java.io.File;
21
24
  import java.io.IOException;
22
25
  import java.io.InputStream;
23
26
  import java.util.HashMap;
27
+ import java.util.Locale;
24
28
  import java.util.Map;
25
29
  import java.util.WeakHashMap;
26
30
 
@@ -62,7 +66,10 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
62
66
  OkHttpClient sharedClient = OkHttpClientProvider.getOkHttpClient();
63
67
  OkHttpClient.Builder builder = sharedClient
64
68
  .newBuilder()
65
- .addInterceptor(createInterceptor(progressListener));
69
+ .addInterceptor(createInterceptor(progressListener))
70
+ // A network interceptor, so it runs before the HTTP cache of
71
+ // `web` images stores the response (checking it reads it).
72
+ .addNetworkInterceptor(createNonImageInterceptor(registry, glide.getArrayPool()));
66
73
  // React Native's shared client comes with an empty cookie jar (React
67
74
  // Native only fills it in for its networking and Image clients), so
68
75
  // images were loaded without the app's cookies, unlike on iOS. Use the
@@ -71,8 +78,7 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
71
78
  builder.cookieJar(new JavaNetCookieJar(new FastImageCookieHandler(context)));
72
79
  }
73
80
  OkHttpClient client = builder.build();
74
- OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client);
75
- registry.replace(GlideUrl.class, InputStream.class, factory);
81
+ registry.replace(GlideUrl.class, InputStream.class, new UrlLoaderFactory(client));
76
82
 
77
83
  // `cache: 'web'` skips Glide's caches and relies on HTTP caching, so
78
84
  // those urls get a client with an HTTP cache (#280): one of their own,
@@ -85,6 +91,40 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
85
91
  registry.prepend(FastImageWebGlideUrl.class, InputStream.class, new WebUrlLoaderFactory(webClient));
86
92
  }
87
93
 
94
+ // Loads GlideUrls with the given client, except `web` ones
95
+ // (FastImageWebGlideUrls): Glide gives a model to the loaders of its
96
+ // superclasses too, and tries the next one when a load fails, so a `web`
97
+ // image that failed (e.g. a 404) was requested again with this client,
98
+ // without the HTTP cache, which also hid the failure.
99
+ private static class UrlLoaderFactory implements ModelLoaderFactory<GlideUrl, InputStream> {
100
+ private final OkHttpClient client;
101
+
102
+ UrlLoaderFactory(OkHttpClient client) {
103
+ this.client = client;
104
+ }
105
+
106
+ @NonNull
107
+ @Override
108
+ public ModelLoader<GlideUrl, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
109
+ final OkHttpUrlLoader loader = new OkHttpUrlLoader(client);
110
+ return new ModelLoader<GlideUrl, InputStream>() {
111
+ @Override
112
+ public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) {
113
+ return loader.buildLoadData(model, width, height, options);
114
+ }
115
+
116
+ @Override
117
+ public boolean handles(@NonNull GlideUrl model) {
118
+ return !(model instanceof FastImageWebGlideUrl);
119
+ }
120
+ };
121
+ }
122
+
123
+ @Override
124
+ public void teardown() {
125
+ }
126
+ }
127
+
88
128
  // Loads FastImageWebGlideUrls with the given client.
89
129
  private static class WebUrlLoaderFactory implements ModelLoaderFactory<FastImageWebGlideUrl, InputStream> {
90
130
  private final OkHttpClient client;
@@ -115,6 +155,50 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
115
155
  }
116
156
  }
117
157
 
158
+ // Fails a successful response that's clearly not an image: its type isn't
159
+ // an image's (text, JSON, XML other than SVG) and its first bytes aren't a
160
+ // format Glide recognizes, e.g. a captive portal's or a proxy's HTML page
161
+ // sent with status 200. Glide keeps the bytes it downloads in its disk
162
+ // cache before decoding them (and doesn't remove them when that fails), as
163
+ // does the HTTP cache of `web` images, so every later load of the url
164
+ // would fail too. Neither caches a failed response. Images sent with the
165
+ // wrong type still load.
166
+ private static Interceptor createNonImageInterceptor(final Registry registry, final ArrayPool arrayPool) {
167
+ return new Interceptor() {
168
+ @NonNull
169
+ @Override
170
+ public Response intercept(@NonNull Chain chain) throws IOException {
171
+ Response response = chain.proceed(chain.request());
172
+ ResponseBody body = response.body();
173
+ MediaType type = body == null ? null : body.contentType();
174
+ if (!response.isSuccessful() || type == null || !isNotImageType(type)) {
175
+ return response;
176
+ }
177
+ ImageHeaderParser.ImageType imageType;
178
+ try (InputStream stream = response.peekBody(64 * 1024).byteStream()) {
179
+ imageType = ImageHeaderParserUtils.getType(
180
+ registry.getImageHeaderParsers(), stream, arrayPool);
181
+ }
182
+ if (imageType != ImageHeaderParser.ImageType.UNKNOWN) {
183
+ return response;
184
+ }
185
+ response.close();
186
+ throw new IOException("Not an image (Content-Type: " + type + ")");
187
+ }
188
+ };
189
+ }
190
+
191
+ private static boolean isNotImageType(MediaType type) {
192
+ String subtype = type.subtype();
193
+ if ("text".equalsIgnoreCase(type.type())) return true;
194
+ if (!"application".equalsIgnoreCase(type.type())) return false;
195
+ return subtype.equalsIgnoreCase("json")
196
+ || subtype.toLowerCase(Locale.ROOT).endsWith("+json")
197
+ || subtype.equalsIgnoreCase("xml")
198
+ || subtype.equalsIgnoreCase("xhtml+xml")
199
+ || subtype.equalsIgnoreCase("javascript");
200
+ }
201
+
118
202
  private static Interceptor createInterceptor(final ResponseProgressListener listener) {
119
203
  return new Interceptor() {
120
204
  @Override
@@ -61,4 +61,10 @@ typedef NS_ENUM(NSInteger, FFFCacheControl) {
61
61
  // responses. 50 MB, as on Android.
62
62
  + (NSURLCache *)webURLCache;
63
63
 
64
+ // After a load of this source failed: for a `cache: 'web'` image whose data
65
+ // isn't an image (e.g. a captive portal's HTML page), removes the response
66
+ // from the HTTP cache, which stored it as it downloaded. Otherwise the next
67
+ // loads would get it from there until it expires, and fail again.
68
+ - (void)forgetResponseAfterError:(NSError *)error;
69
+
64
70
  @end
@@ -2,6 +2,7 @@
2
2
  #import <SDWebImage/SDWebImageDownloader.h>
3
3
  #import <SDWebImage/SDWebImageDownloaderResponseModifier.h>
4
4
  #import <SDWebImage/SDWebImageDownloaderDecryptor.h>
5
+ #import <SDWebImage/SDWebImageError.h>
5
6
 
6
7
  static NSUInteger const FFFWebCacheSize = 50 * 1024 * 1024;
7
8
 
@@ -79,6 +80,17 @@ static NSUInteger const FFFWebCacheSize = 50 * 1024 * 1024;
79
80
  return downloader;
80
81
  }
81
82
 
83
+ - (void)forgetResponseAfterError:(NSError *)error
84
+ {
85
+ if (_cacheControl != FFFCacheControlWeb || !_url) {
86
+ return;
87
+ }
88
+ if (![error.domain isEqualToString:SDWebImageErrorDomain] || error.code != SDWebImageErrorBadImageData) {
89
+ return;
90
+ }
91
+ [[FFFastImageSource webURLCache] removeCachedResponseForRequest:[NSURLRequest requestWithURL:_url]];
92
+ }
93
+
82
94
  + (NSURLCache *)webURLCache
83
95
  {
84
96
  static NSURLCache *cache;
@@ -579,6 +579,9 @@ NSString *FFFErrorMessage(NSError *error)
579
579
  setImageBlock: setImage
580
580
  progress: nil
581
581
  completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
582
+ if (error) {
583
+ [source forgetResponseAfterError: error];
584
+ }
582
585
  if (error && weakSelf.loadCount == load) {
583
586
  // Keeps the image it has, without trying again on every
584
587
  // layout (e.g. while offline).
@@ -608,6 +611,9 @@ NSString *FFFErrorMessage(NSError *error)
608
611
  SDImageCacheType cacheType,
609
612
  BOOL finished,
610
613
  NSURL* _Nullable imageURL) {
614
+ if (error) {
615
+ [source forgetResponseAfterError: error];
616
+ }
611
617
  // Replaced by another load (a new source, or the same one
612
618
  // restarted for a new size), which sends the events. This
613
619
  // one was cancelled, which SDWebImage reports as an error.
@@ -107,7 +107,10 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
107
107
  }
108
108
  [results addObject:[NSNull null]];
109
109
  // A source's own priority replaces the prefetcher's, as on Android.
110
- SDWebImageOptions options = prefetcherOptions;
110
+ // Failed urls are tried again, as views do (SDWebImage otherwise
111
+ // fails a url that failed before, e.g. with data that isn't an
112
+ // image, without a request until the app is relaunched, #394).
113
+ SDWebImageOptions options = prefetcherOptions | SDWebImageRetryFailed;
111
114
  if (source.hasPriority) {
112
115
  options &= ~(SDWebImageLowPriority | SDWebImageHighPriority);
113
116
  if (source.priority == FFFPriorityLow) {
@@ -137,6 +140,9 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
137
140
  return;
138
141
  }
139
142
  done = YES;
143
+ if (error) {
144
+ [source forgetResponseAfterError:error];
145
+ }
140
146
  results[idx] = image
141
147
  ? @{@"ok": @YES, @"width": @(image.size.width), @"height": @(image.size.height)}
142
148
  : @{@"ok": @NO, @"error": FFFErrorMessage(error)};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.17.0",
3
+ "version": "8.17.2",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",