react / react/react-native

TypeError: Network request failed randomly in IOS

未关闭
#49,037 20 条评论 1 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

Needs: Attention Needs: Repro
主要语言
C++
星标
127k
派生
25.3k
平均合并
1 天 23 小时
30 天内合并 PR
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. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

报告指出了 api.config(其中配置了 fetchBaseQuery 和拦截器)以及 profile.js(其中运行了三个 RTK Query 请求)。首先,在检查请求是否到达服务器的同时,复现 iOS 上的间歇性失败,然后比较失败设备和成功设备。原因被隔离并且修复得到验证,即表示完成。

由索引模型根据 Issue 内容生成。

评估

技术栈
ios, javascript, react-native
领域
api, mobile, networking
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
冷清
描述清晰度
需要澄清
新手友好度
35/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。