react / react/react-native

TypeError: Network request failed randomly in IOS

オープン
#49,037 コメント 20 件 リアクション 1 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

Needs: Attention Needs: Repro
主要言語
C++
スター
127k
フォーク
25.3k
平均マージ
1日 23時間
マージ済み PR(30日)
4

説明

Description

some users reported network request failed using rtk query or TypeError: Network request failed randomly for some api's and others works fine , according to this issue opened by me in redux toolkit link , the issue arises from react native environment , this issue happen randomly in ios , i mean same user in other device can request this api and works fine , the request not even reaches to our server.

api.config

import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react';
import Config from 'react-native-config';
import {I18nManager, Platform} from 'react-native';
import {getToken, getGuestPublicToken} from '@selectors';
import qs from 'qs';
import {getAppVersion} from '@utils/helpers';

const baseQuery = fetchBaseQuery({
  baseUrl: `${Config.API_URL}`,
  paramsSerializer: params => qs.stringify(params, {arrayFormat: 'brackets'}),
});

const baseQueryWithInterceptor = async (args, api, extraOptions) => {
  const version = (await getAppVersion()).substring(1).split(' ')[0];
  const state = api.getState();
  const privateToken = getToken(state);
  const publicToken = getGuestPublicToken(state);

  args.headers = {
    ...args.headers,
    'Accept-Language': I18nManager.isRTL ? 'ar' : 'en',
    'User-Agent': Platform.OS,
    'App-Version': version,
    Authorization: `Bearer ${
      privateToken ? privateToken?.access_token : publicToken?.access_token
    }`,
  };
  let result = await baseQuery(args, api, extraOptions);

  if (__DEV__) {
    console.log(api.endpoint, args, result);
  }

  return {
    ...result,
    error: result?.error?.data || result?.error,
  };
};

export const api = createApi({
  baseQuery: baseQueryWithInterceptor,
  endpoints: () => ({}),
  invalidationBehavior: 'immediately',
  tagTypes: [
    'Customer',
    'order',
    'Cards',
    'ActiveCoupons',
    'ExpiredCoupons',
    'Wallet',
    'WalletHistory',
    'PromoCode',
    'GetRunningOrder',
    'Banners',
    'ContactMessages',
  ],
});

profile.js

import {Animated, StyleSheet, View, Image, RefreshControl} from 'react-native';
import React, {memo, useCallback, useMemo, useRef, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {useNavigation} from '@react-navigation/native';
import {useSelector} from 'react-redux';
import {getToken} from '@selectors';
import {
  useGetCustomerQuery,
  useGetQuestionnaireQuery,
  useGetWalletQuery,
} from '@services';
import {
  ProfileButton,
  ProfileInfo,
  QuestionnaireCard,
  ReferCard,
  Button,
  EmptyView,
  ErrorView,
  LoadingView,
  Header,
  CheckMobileVerified,
} from '@components';
import {
  crypto,
  profile_heart,
  wallet2,
  edit,
  profile2,
  region,
  coupon,
  credit2,
  tims_bag,
} from '@assets';
import {moderateScale, moderateVerticalScale} from 'react-native-size-matters';
import {colors, typo} from '@common';
import {TouchableOpacity} from 'react-native-gesture-handler';
import Modal from 'react-native-modal';
import {convertEnglishNumbersToArabic} from '@utils';
import {commonStyles} from '@utils/constants';

const EditProfileButton = memo(() => {
  const {navigate} = useNavigation();
  return (
    <TouchableOpacity
      onPress={() => navigate('EditProfile')}
      style={styles.item}>
      <Image style={styles.editIcon} source={edit} />
    </TouchableOpacity>
  );
});
const EditCountryButton = memo(() => {
  const {navigate} = useNavigation();
  return (
    <TouchableOpacity onPress={() => navigate('Country')} style={styles.item}>
      <Image style={styles.editIcon} source={region} tintColor={colors.white} />
    </TouchableOpacity>
  );
});

const Profile = () => {
  const {t} = useTranslation();
  const navigation = useNavigation();
  const isLoggedIn = useSelector(getToken);
  const [isModalVisible, setModalVisible] = useState(false);

  const {
    data,
    isLoading: isLoadingQuestionnaire,
    isError: isLoadingQuestionnaireError,
    refetch: refetchQuestionnaire,
    error: gettingQuestionnaireError,
  } = useGetQuestionnaireQuery(undefined, {skip: !isLoggedIn});

  const toggleModal = () => {
    setModalVisible(!isModalVisible);
  };
  const {
    currentData: user = {},
    refetch: refetchCustomer,
    isLoading: isGettingCustomer,
    isFetching: isFetchingCustomer,
    isError: isGettingCustomerError,
    error: gettingCustomerError,
  } = useGetCustomerQuery(undefined, {
    skip: !isLoggedIn,
  });
  const {
    data: {user_wallet} = {},
    isLoading: isFetchingWallet,
    isError: isFetchingWalletError,
    refetch: refetchWallet,
    error: gettingWalletError,
  } = useGetWalletQuery(undefined, {
    skip: !isLoggedIn,
  });
  const isLoading = useMemo(
    () =>
      [isLoadingQuestionnaire, isGettingCustomer, isFetchingWallet].some(
        Boolean,
      ),
    [isLoadingQuestionnaire, isGettingCustomer, isFetchingWallet],
  );
  const hasError = useMemo(
    () =>
      [
        isLoadingQuestionnaireError,
        isGettingCustomerError,
        isFetchingWalletError,
      ].some(Boolean),
    [
      isLoadingQuestionnaireError,
      isGettingCustomerError,
      isFetchingWalletError,
    ],
  );

  const error = useMemo(
    () =>
      [
        gettingQuestionnaireError,
        gettingCustomerError,
        gettingWalletError,
      ].find(Boolean),
    [gettingQuestionnaireError, gettingCustomerError, gettingWalletError],
  );

  const scrollY = useRef(new Animated.Value(0)).current;
  const isScrolling = useRef(false);

  const handleScrollBeginDrag = () => {
    isScrolling.current = true;
  };
  const navigationFromProfile = useCallback(
    screenName => {
      navigation.navigate(screenName);
    },
    [navigation],
  );

  const emptyViewActions = useMemo(
    () => [
      {
        title: t('sign_in').toUpperCase(),
        type: Button.types.PRIMARY,
        onPress: () =>
          navigation.navigate('AuthStack', {
            screen: 'Login',
          }),
      },
    ],
    [navigation],
  );

  const image = useMemo(
    () => (user?.avatar ? {uri: user?.avatar} : profile2),
    [user],
  );
  const onRetry = useCallback(
    () =>
      Promise.all([
        isGettingCustomerError && refetchCustomer(),
        isLoadingQuestionnaireError && refetchQuestionnaire(),
        isFetchingWalletError && refetchWallet(),
      ]),
    [
      isGettingCustomerError,
      isLoadingQuestionnaireError,
      refetchCustomer,
      refetchCustomer,
      isFetchingWalletError,
      refetchWallet,
    ],
  );
  const walletInfo = useMemo(
    () =>
      `${t('balance')} : ${convertEnglishNumbersToArabic(
        Number(user_wallet?.total || 0).toFixed(2),
      )} ${user_wallet?.currency}`,
    [user_wallet, convertEnglishNumbersToArabic],
  );
  const opacity = scrollY.interpolate({
    inputRange: [0, 50],
    outputRange: [1, 0],
    extrapolate: 'clamp',
  });
  const scale = scrollY.interpolate({
    inputRange: [0, 50],
    outputRange: [1, 0.8],
    extrapolate: 'clamp',
  });
  const onRefresh = () => {
    refetchCustomer();
  };
  return (
    <>
      <View>
        <Header
          rightItem={isLoggedIn ? EditProfileButton : EditCountryButton}
          title={isLoggedIn ? null : t('profile')}
        />
      </View>
      {isLoggedIn ? (
        <LoadingView isLoading={isLoading} containerStyle={styles.loadingView}>
          <ErrorView hasError={hasError} onRetry={onRetry} error={error}>
            <Animated.View
              style={[
                styles.profileImgContainer,
                {opacity, transform: [{scale}]},
              ]}>
              <View>
                <TouchableOpacity
                  onPress={toggleModal}
                  style={styles.imageContainer}>
                  <View style={styles.profileImage}>
                    <Image source={image} style={styles.image} />
                  </View>
                </TouchableOpacity>
              </View>
            </Animated.View>
            <View style={[styles.mainContainer(user?.verified)]}>
              <Animated.ScrollView
                keyboardShouldPersistTaps="always"
                refreshControl={
                  <RefreshControl refreshing={false} onRefresh={onRefresh} />
                }
                onScroll={Animated.event(
                  [{nativeEvent: {contentOffset: {y: scrollY}}}],
                  {useNativeDriver: true},
                )}
                onScrollBeginDrag={handleScrollBeginDrag}
                scrollEventThrottle={16}
                bounces={false}
                showsVerticalScrollIndicator={false}>
                <View style={styles.container}>
                  <ProfileInfo />
                  <QuestionnaireCard />
                  <View style={styles.separator} />
                  <ReferCard />
                  <ProfileButton
                    icon={tims_bag}
                    label={t('my_orders')}
                    showBorderBottom={true}
                    onPress={() => navigationFromProfile('MyOrders')}
                  />
                  <ProfileButton
                    icon={crypto}
                    label={t('points_analysis')}
                    showBorderBottom={true}
                    onPress={() => navigationFromProfile('PointsAnalysis')}
                  />
                  <ProfileButton
                    icon={profile_heart}
                    label={t('favorites')}
                    showBorderBottom={true}
                    onPress={() => navigationFromProfile('Favorites')}
                  />
                  <ProfileButton
                    icon={credit2}
                    label={t('my_cards')}
                    showBorderBottom={true}
                    onPress={() => navigationFromProfile('MyCards')}
                  />
                  <ProfileButton
                    icon={wallet2}
                    label={t('wallet')}
                    label2={walletInfo}
                    showBorderBottom={true}
                    onPress={() => navigationFromProfile('Wallet')}
                  />
                  <ProfileButton
                    icon={coupon}
                    label={t('Coupons')}
                    showBorderBottom={true}
                    onPress={() => navigationFromProfile('Coupons')}
                  />
                  {/* <ProfileButton
                    icon={location}
                    label={t('my_addresses')}
                    showBorderBottom={true}
                    onPress={() => navigationFromProfile('MyAddresses')}
                  /> */}
                  <ProfileButton
                    icon={region}
                    label={t('country')}
                    icon2={user?.client_country?.country?.image_url}
                    onPress={() => navigationFromProfile('Country')}
                  />
                </View>
              </Animated.ScrollView>
              {!user?.verified && (
                <CheckMobileVerified isMobileVerified={user?.verified} />
              )}
              <Modal isVisible={isModalVisible} onBackdropPress={toggleModal}>
                <View style={styles.overlay}>
                  <View style={styles.modalContainer}>
                    <Image source={image} width="100%" height="100%" />
                  </View>
                </View>
              </Modal>
            </View>
          </ErrorView>
        </LoadingView>
      ) : (
        <View style={styles.fullHeight}>
          <EmptyView
            iconName={'account-alert-outline'}
            iconColor={colors.gray}
            title={t(
              'sign_in_or_register_to_unlock_your_rewards_and_earn_points',
            )}
            actions={emptyViewActions}
            titleStyle={styles.emptyViewTitle}
            iconNameSize={moderateScale(100)}
            actionsContainer={styles.actionsContainer}
          />
        </View>
      )}
    </>
  );
};

export default memo(Profile);

const styles = StyleSheet.create({
  mainContainer: isVerified => ({
    flex: 1,
    backgroundColor: colors.white,
    width: '100%',
    paddingBottom: isVerified ? 0 : moderateVerticalScale(50),
  }),
  loadingView: {
    flex: 1,
  },
  separator: {
    height: moderateVerticalScale(11),
  },
  pleaseLoginText: {
    fontSize: commonStyles.thirdFontSize,
    color: colors.text,
    marginBottom: moderateVerticalScale(20),
    fontFamily: typo.sofiaBroRegular,
  },
  loginButton: {
    width: moderateScale(150),
  },
  container: {
    flex: 1,
    paddingHorizontal: commonStyles.marginHorizontal,
    backgroundColor: colors.white,
    alignItems: 'center',
    alignSelf: 'center',
    width: '100%',
    maxWidth: moderateScale(650),
    marginBottom: commonStyles.marginVertical * 3,
  },
  pleaseLoginContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    gap: moderateVerticalScale(22),
    paddingHorizontal: moderateScale(60),
  },
  pleaseLoginImageText1: {
    fontSize: commonStyles.thirdFontSize,
    color: colors.text,
    fontFamily: typo.helvetica85Heavy,
    marginBottom: moderateVerticalScale(11),
    textAlign: 'center',
  },
  pleaseLoginImageText2: {
    fontSize: commonStyles.thirdFontSize,
    color: colors.text,
    fontFamily: typo.sofiaBroRegular,
    textAlign: 'center',
  },
  pleaseLoginImage: {
    width: moderateScale(100),
    height: moderateVerticalScale(100),
    resizeMode: 'contain',
    tintColor: colors.drift_wood,
  },
  pleaseLoginImageButton: {
    width: '50%',
  },
  editIcon: {
    height: moderateVerticalScale(30),
    width: moderateScale(30),
    resizeMode: 'contain',
    tintColor: colors.white,
  },
  item: {
    paddingHorizontal: commonStyles.marginHorizontal / 2,
  },

  addImage: {
    width: moderateScale(27),
    height: moderateVerticalScale(27),
    position: 'absolute',
    bottom: moderateVerticalScale(0),
    right: moderateScale(5),
  },
  camera: {
    width: moderateScale(35),
    height: moderateVerticalScale(35),
    resizeMode: 'contain',
  },
  image: {
    width: moderateScale(90),
    height: moderateVerticalScale(90),
    borderRadius: moderateScale(45),
    resizeMode: 'cover',
  },
  profileImgContainer: {
    position: 'absolute',
    zIndex: 99999999,
    width: '50%',
    flexDirection: 'row',
    justifyContent: 'center',
    top: '-6%',
    left: '25%',
  },
  overlay: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  modalContainer: {
    width: moderateScale(300),
    height: moderateVerticalScale(300),
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
    backgroundColor: colors.black,
    borderRadius: moderateScale(150),
  },
  fullHeight: {
    flex: 1,
  },
  emptyViewTitle: {
    fontFamily: typo.sofiaBroRegular,
    fontSize: commonStyles.thirdFontSize,
  },
  actionsContainer: {position: 'absolute', bottom: '4%'},
  profileImage: {
    width: moderateScale(90),
    height: moderateVerticalScale(90),
    backgroundColor: colors.black,
    borderRadius: moderateScale(45),
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  },

  imageContainer: {
    backgroundColor: colors.white,
    borderRadius: moderateScale(150),
    padding: commonStyles.marginHorizontal / 3,
  },
});

Errorview.jsx

import React, {memo} from 'react';
import {useTranslation} from 'react-i18next';
import {StyleSheet, Text, View} from 'react-native';
import Button from './Button';
import {moderateVerticalScale} from 'react-native-size-matters/extend';
import {colors, typo} from '@common';
import {commonStyles} from '@utils/constants';

const ErrorView = ({children, hasError, onRetry, error}) => {
  const {t} = useTranslation();

  const generateErrorMsg = error => {
    switch (error?.originalStatus || error?.status) {
      case 'FETCH_ERROR':
        return t('check_your_internet_connection');
      case 502:
        return t('we_are_updating_our_services_thanks_for_your_patience');
      default:
        return error?.error;
    }
  };

  return hasError ? (
    <View style={styles.container}>
      <View style={styles.oopsContainer}>
        <Text style={styles.oops}>{t('oops')}</Text>
        <Text style={styles.wentWrong}>
          {generateErrorMsg(error) || t('something_went_wrong')}
        </Text>
      </View>
      <Button
        title={t('try_again')}
        onPress={onRetry}
        style={styles.tryAgain}
      />
    </View>
  ) : (
    children
  );
};

export default memo(ErrorView);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: colors.white,
    paddingHorizontal: commonStyles.marginHorizontal,
  },
  someThingWentWrong: {
    fontFamily: typo.sofiaBroBlack,
    fontSize: commonStyles.thirdFontSize,
    paddingBottom: moderateVerticalScale(25),
  },
  oopsContainer: {
    paddingBottom: moderateVerticalScale(24),
    gap: moderateVerticalScale(10),
  },
  oops: {
    fontFamily: typo.sofiaBroBlack,
    textAlign: 'center',
    fontSize: commonStyles.semiBiggerFontSize,
    color: colors.black,
  },
  wentWrong: {
    fontFamily: typo.sofiaBroRegular,
    textAlign: 'center',
    fontSize: commonStyles.semiBiggerFontSize,
    color: colors.black,
    lineHeight: commonStyles.secondaryLingHeight,
  },
  tryAgain: {
    backgroundColor: colors.main_red,
  },
});

Steps to reproduce
  1. open app
  2. navigate to profile stack
  3. Use RTK Query to initiate a network request (e.g., useQuery)
  4. Observe that the query returns a TypeError: Network request failed randomly randomly instead of executing successfully.
React Native Version

0.75.4

Affected Platforms

Runtime - iOS

Output of npx react-native info
System:
  OS: macOS 12.7.6
  CPU: (8) x64 Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
  Memory: 53.96 MB / 16.00 GB
  Shell:
    version: 5.8.1
    path: /bin/zsh
Binaries:
  Node:
    version: 20.15.0
    path: ~/.nvm/versions/node/v20.15.0/bin/node
  Yarn:
    version: 4.5.0
    path: ~/.nvm/versions/node/v20.15.0/bin/yarn
  npm:
    version: 10.7.0
    path: ~/.nvm/versions/node/v20.15.0/bin/npm
  Watchman:
    version: 2024.07.15.00
    path: /usr/local/bin/watchman
Managers:
  CocoaPods:
    version: 1.15.2
    path: /usr/local/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 21.4
      - iOS 16.0
      - macOS 12.3
      - tvOS 16.0
      - watchOS 9.0
  Android SDK:
    API Levels:
      - "25"
      - "28"
      - "30"
      - "31"
      - "33"
      - "34"
      - "35"
    Build Tools:
      - 24.0.0
      - 24.0.1
      - 24.0.2
      - 24.0.3
      - 30.0.2
      - 30.0.3
      - 33.0.0
      - 33.0.1
      - 34.0.0
      - 35.0.0
    System Images:
      - android-26 | Google Play Intel x86 Atom
      - android-34 | Intel x86_64 Atom
      - android-34 | Google APIs Intel x86_64 Atom
    Android NDK: Not Found
IDEs:
  Android Studio: 2024.1 AI-241.18034.62.2411.12169540
  Xcode:
    version: 14.0/14A309
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.11
    path: /usr/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.3.1
    wanted: 18.3.1
  react-native:
    installed: 0.75.4
    wanted: 0.75.4
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: true
  newArchEnabled: false
Stacktrace or Logs
TypeError: Network request failed randomly
Reproducer

commercial app

Screenshots and Videos

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

レポートでは、fetchBaseQuery とインターセプターが設定されている api.config、および3つの RTK Query リクエストが実行される profile.js が示されています。まず、リクエストがサーバーに到達しているかを確認しながら iOS で断続的な失敗を再現し、その後、失敗するデバイスと成功するデバイスを比較してください。原因が切り分けられ、修正が検証されれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
ios, javascript, react-native
領域
api, mobile, networking
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
静か
明瞭さ
説明が足りない
初心者へのやさしさ
35/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。