dart-lang / http

A composable API for making HTTP requests in Dart.
https://pub.dev/packages/http
BSD 3-Clause "New" or "Revised" License
1.02k stars 354 forks source link

http calls occasionally running into a socket exception until wifi reset #642

Closed gutisalex closed 2 years ago

gutisalex commented 2 years ago

Info

Device: Xiaomi Redmi 4 OS: MIUI Global 11.0.2 Flutter: 2.5.3 Dart: 2.14.4 http: 0.13.4

Description

I have built an app which is working along with an Alcatel Wifi Access router. The app is supposed to show infos about the reception, connected devices and the batterie of the router. The router is also capable of receiving SMS which can be fetched, read and replied. I use API polling to get the information and update it periodically every 30 seconds. The same goes with the sms but I fetch them every 2 seconds. I made this app so I don't need to use the web interface and so far its actually working well. But from time to time all my http calls are running into a socket exception and the only solution is turning off and on the wifi connection. But it is only the app that cannot reach the router anymore because if I use the browser and the web-interface everything is still working!

The problem is that this occurs totally random, sometimes it happens and I can reset the wifi connection and it will happen straight again but sometimes it just runs for hours without any problems.

It first looks like the http client just crashes for some reason but if it happens I can still make calls from out the app to the internet (over the wifi connection of the router). So my guess is that the http client closes the connection to the router only and discards all future calls until the wifi is being reset. Now I just need to figure out what it is causing this?! Do I make too many calls in too short period of time?

Code

This is my router_api_client.dart which is part of a bloc layer:

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as httpClient;
import 'package:test_wlan/constants/router_error_messages.dart'
    as RouterErrorMessages;
import 'package:test_wlan/constants/router_exceptions.dart'
    as RouterExceptions;
import 'package:test_wlan/constants/router_settings.dart' as router;
import 'package:test_wlan/data/models/connected_device.dart';
import 'package:test_wlan/data/models/router_api_exception.dart';
import 'package:test_wlan/data/models/router_api_response.dart';
import 'package:test_wlan/data/models/sms_contact.dart';
import 'package:test_wlan/data/models/sms_content_list_result.dart';
import 'package:test_wlan/data/models/sms_storage_state.dart';

/// As the lowest layer in this app's architecture, [RouterApiClient]'s
/// responsibility is only to fetch data directly from API.
class RouterApiClient {
  static const _baseUrl = router.BASE_URL;
  static const _api = router.API;
  static const _apiReferrer = "http://mw40.home/index.html";
  static const _requestVerificationKey =
      "KSDHSDFOGQ5WERYTUIQWERTYUISDFG1HJZXCVCXBN2GDSMNDHKVKFsVBNf";
  static const _contentType = "application/json; charset=UTF-8";

  String _token = '';

  Map<String, String> get headers {
    return {
      'Content-type': _contentType,
      '_TclRequestVerificationKey': _requestVerificationKey,
      '_TclRequestVerificationToken': _token,
      'Referer': _apiReferrer,
    };
  }

  Map<String, dynamic> _getBody(
    String method,
    dynamic params,
    String id,
  ) {
    return {
      'jsonrpc': '2.0',
      'method': method,
      'params': params,
      'id': id,
    };
  }

  /// basic function for sending a request with [data] to the router
  Future<RouterApiResponse> postData(Map<String, dynamic> data) async {
    late RouterApiResponse apiResponse;

    String requestBody = json.encode(data);
    var uri = Uri.http(_baseUrl, _api);

    try {
      /// send request
      var response =
          await httpClient.post(uri, body: requestBody, headers: headers);

      final json = jsonDecode(utf8.decode(response.bodyBytes));

      /// model response
      apiResponse = RouterApiResponse.fromJson(json);

      /// handle router error response
      if (apiResponse.error != null) {
        throw RouterApiException(
            code: apiResponse.error!.code, message: apiResponse.error!.message);
      }
    } on SocketException {
      /// Response that comes when device is not connected to the router wifi

      print('socket exception');
      throw RouterApiException(
        code: RouterExceptions.NoConnection,
        message: RouterErrorMessages.NoConnection,
      );
    }
    return apiResponse;
  }

  /// Authenticate the app with the router
  Future<void> login(String username, String password) async {
    final params = {
      'UserName': username,
      'Password': password,
    };

    final body = _getBody('Login', params, '1.1');

    final loginResponse = await postData(body);

    _token = loginResponse.result!['token'].toInt().toString();
  }

  /// Get Login State, 1 for logged in and 0 for logged out
  Future<int> getLoginState() async {
    final body = _getBody('GetLoginState', null, '1.3');

    final response = await postData(body);

    return response.result!['State'].toInt();
  }

  /// Send heart beat
  Future<void> sendHeartBeat() async {
    final Map<String, dynamic> body = {
      'jsonrpc': '2.0',
      'method': 'HeartBeat',
      'params': {},
      'id': '1.5',
    };

    await postData(body);
  }

  /// Get device name of the router
  Future<dynamic> getSystemInfo() async {
    final Map<String, dynamic> body = {
      'jsonrpc': '2.0',
      'method': 'GetSystemInfo',
      'params': {},
      'id': '13.1',
    };

    final response = await postData(body);

    return response.result;
  }

  /// Get battery level
  Future<int> getBatteryLevel() async {
    final Map<String, dynamic> body = {
      'jsonrpc': '2.0',
      'method': 'GetSystemStatus',
      'params': {},
      'id': '13.4',
    };
    final response = await postData(body);

    return response.result!['bat_cap'].toInt();
  }

  /// Get network quality
  Future<int> getNetworkQuality() async {
    final Map<String, dynamic> body = {
      'jsonrpc': '2.0',
      'method': 'GetNetworkInfo',
      'params': {},
      'id': '4.1',
    };
    final response = await postData(body);

    return response.result!['SignalStrength'].toInt();
  }

  /// Get device list as a list with items from type [ConnectedDevice]
  Future<List<ConnectedDevice>> getConnectedDevices() async {
    final Map<String, dynamic> body = {
      'jsonrpc': '2.0',
      'method': 'GetConnectedDeviceList',
      'params': {},
      'id': '12.1',
    };
    final response = await postData(body);

    List<ConnectedDevice> deviceList = [];
    for (dynamic device in response.result!['ConnectedList']) {
      deviceList.add(ConnectedDevice.fromJson(device));
    }

    return deviceList;
  }

  /// Get network info
  /// [ConnectionStatus] = 0|1 => Connection to the internet
  Future<int> getConnectionState() async {
    final Map<String, dynamic> body = {
      'jsonrpc': '2.0',
      'method': 'GetConnectionState',
      'params': {},
      'id': '3.1',
    };
    final response = await postData(body);

    return response.result!['ConnectionStatus'].toInt();
  }

  /// Get SMS Storage State
  Future<SmsStorageState> getSmsStorageState() async {
    final body = {
      'jsonrpc': '2.0',
      'method': 'GetSMSStorageState',
      'params': {},
      'id': '6.4',
    };

    final response = await postData(body);

    return SmsStorageState.fromJson(response.result);
  }

  /// Get SMS Contact List
  /// Returns a list of all contact sms
  Future<List<SmsContact>> getSmsContactList(int page) async {
    final body = {
      'jsonrpc': '2.0',
      'method': 'GetSMSContactList',
      'params': {"Page": page},
      'id': '6.3',
    };
    final response = await postData(body);
    final smsContactList = response.result!['SMSContactList']
        .map<SmsContact>((smsContact) => SmsContact.fromJson(smsContact))
        .toList();

    return smsContactList;
  }

  /// Get SMS Content List
  Future<SmsContentListResult> getSmsContentList(
      int page, int contactId) async {
    final body = {
      'jsonrpc': '2.0',
      'method': 'GetSMSContentList',
      'params': {"Page": page, "ContactId": contactId},
      'id': '6.3',
    };
    final response = await postData(body);

    final SmsContentListResult smsContentListResult =
        SmsContentListResult.fromJson(response.result!);

    return smsContentListResult;
  }

  /// Get SMS Content List
  Future<void> sendSms(
    int smsId,
    String smsContent,
    List<String> phoneNumber,
    String smsTime,
  ) async {
    final params = {
      "SMSId": smsId,
      "SMSContent": smsContent,
      "PhoneNumber": phoneNumber,
      "SMSTime": smsTime
    };
    final body = _getBody('SendSMS', params, '6.3');
    await postData(body);
  }

  /// Delete SMS
  /// DelFlag: This flag means the SMS that want to delete.
  ///   0: delete all SMS
  ///   1: delete one record in Contact SMS list
  ///   2: delete one record in Content SMS list
  /// ContactId: This id must be in Contact SMS list.
  /// SMSId: This id must be Content SMS list.
  Future<void> deleteSms(int delFlag, [int? contactId, int? smsId]) async {
    final params = {
      "DelFlag": delFlag,
      "ContactId": contactId,
      "SMSId": smsId,
    };
    final body = _getBody('DeleteSMS', params, '6.5');
    await postData(body);
  }
}
gutisalex commented 2 years ago

The solution of this was using the IP of the wifi router instead of using its domainname. Eventhough I could use the domainname in the browser to still connect to the routers interface, the app on the other hand could not find the router anymore. I figured that out using DIO because I wanted to test if the http package not working correctly but apparently that was not the issue. After using the ip the error did not appear anymore on both clients (tested with one call that was causing the issue by 100%). My guess is something crashes with domain name resolution within flutter or even within the android sdk but that is going far over my knowledge... so I will close this issue for me!