We are aware of the issue with the badge emails resending to everyone, we apologise for the inconvenience - learn more here.

Forum Discussion

pear0533's avatar
pear0533
Explorer | Level 3
2 years ago

Dropbox API Hangs During Large File Download

Hey there! I am currently maintaining a mod launcher software that utilizes the Dropbox .NET C# API to allow users to download mod files.

 

Unfortunately, large files (usually 2-5GB in size) do not download completely while using the launcher, hanging mid-way through the download and refusing to complete, even if a download is restarted promptly. However, this issue only happens for a select few people who are using the launcher software, which makes it appear as though Dropbox is enforcing a quota or limit on the number of concurrent downloads that are able to be processed at a time. This would explain why, for one particular user, the download stopped at around 45%, while for another, it hung at roughly 65%.

 

I have had a conversation with someone who seemed to be affiliated with the .NET C# API team prior to making this post a few months back, named Hubert, and they suggested using range requests to tackle the issue. However, not even range requests seemed to do the trick, as a given download would end up becoming stuck yet again after some time, with stream-related errors bubbling up for no meaningful reason.

 

I am fully well-aware that Dropbox is NOT intended to serve as a content-delivery network, however the support provided for the .NET C# API is somewhat ridiculous.

 

Currently, I am using V2 of the Dropbox .NET SDK, Version 6.36.0.  Line 46 is where an unexpected stream error is occurring within the downloader code (included in this post), wherein the code attempts to read data from the Stream object returned by a single DownloadAsync() API call from the DropboxClient class before the do while loop begins (the DownloadAsync() function is coherently abstracted as DownloadFile() in a wrapper class for the DropboxClient named DropboxFileManager, to interact with the API more efficiently). The error reads the following: Unable to read data from the transport connection: The connection was closed.

 

Attached are some samples of code currently being used in my launcher software related to downloading. I would really appreciate it if anyone could shed some slight on this:

 

DropboxClient Custom HttpClient Configuration:

 

 

 

    public DropboxFileManager()
    {
        var configManager = new LauncherConfigManager();
        RefreshToken = configManager.LauncherConfig[DropboxRefreshTokenKey]?.ToString();
        ClientId = configManager.LauncherConfig[DropboxClientIdKey]?.ToString();
        ClientSecret = configManager.LauncherConfig[DropboxClientSecretKey]?.ToString();
        var httpClient = new HttpClient(new WebRequestHandler { ReadWriteTimeout = 10 * 1000 }) { Timeout = TimeSpan.FromMinutes(1000) };
        var clientConfig = new DropboxClientConfig($"{NativeManifest.ExecutableAppName}") { HttpClient = httpClient };
        _client = new DropboxClient(RefreshToken, ClientId, ClientSecret, clientConfig);
    }

 

 

 

Downloader code (no need to tell me that this needs refactoring, I am well-aware that it does):

 

 

 

    private async Task<LibraryItemCard> DownloadModFileAndUpdateCardStatus(LibraryItemCard card, Mod mod, string uri, string localPath)
    {
        mod.IsBusy = true;
        await Task.Run(async () =>
        {
            while (mod.IsBusy)
            {
                mod.IsQueuing = true;
                card = await UpdateItemCardProgress(card, mod);
                mod.IsQueuing = false;
                IDownloadResponse<FileMetadata> response = await _fileManager.DownloadFile(uri);
                if (response == null)
                {
                    mod.IsReconnecting = true;
                    card = await UpdateItemCardProgress(card, mod);
                    mod.IsReconnecting = false;
                    await Task.Delay(4000);
                    continue;
                }
                Stream stream = await response.GetContentAsStreamAsync();
                var modFileZipPath = $"{localPath}.zip";
                FileStream fileStream;
                try
                {
                    fileStream = File.OpenWrite(modFileZipPath);
                }
                catch
                {
                    ShowErrorDialog(ZipAccessError);
                    continue;
                }
                var prevProgress = 0;
                int streamBufferLength;
                var resumeTask = false;
                do
                {
                    if (mod.IsCancellingDownload || !IsCurrentPage(MainPageName) && !IsCurrentPage(SettingsPageName))
                    {
                        fileStream.Close();
                        stream.Close();
                        if (!mod.IsCancellingDownload) return card;
                        if (File.Exists(modFileZipPath)) File.Delete(modFileZipPath);
                        mod.IsBusy = false;
                        await RunBackgroundAction(() => card = Refre**bleep**emCard(card, mod));
                        mod.IsCancellingDownload = false;
                        return card;
                    }
                    var streamBuffer = new byte[1024];
                    try
                    {
                        streamBufferLength = await stream.ReadAsync(streamBuffer, 0, 1024);
                    }
                    catch
                    {
                        fileStream.Close();
                        resumeTask = true;
                        break;
                    }
                    try
                    {
                        fileStream.Write(streamBuffer, 0, streamBufferLength);
                    }
                    catch
                    {
                        ShowErrorDialog(NotEnoughSpaceError);
                        return card;
                    }
                    mod.Progress = (int)((double)fileStream.Length / response.Response.Size * 100);
                    if (mod.Progress == prevProgress) continue;
                    prevProgress = mod.Progress;
                    if (!mod.IsOptionsButtonActivated) card = await UpdateItemCardProgress(card, mod);
                } while (stream.CanRead && streamBufferLength > 0);
                if (resumeTask) continue;
                fileStream.Close();
                card = await ExtractModAndUpdateCardStatus(card, mod, modFileZipPath);
                await RunBackgroundAction(() => card = Refre**bleep**emCard(card, mod));
                mod.IsBusy = false;
                if (mod.IsUpdated) mod.Configure(FocusedGame);
            }
            return card;
        });
        return card;
    }

 

 

 

Again, any insight is much appreciated.

  • Greg-DB's avatar
    Greg-DB
    Icon for Dropbox Staff rankDropbox Staff

    It sounds like you already opened a support ticket for this, so you can continue to using that existing ticket, to keep the conversation in one place. I'm happy to help here however I can if you prefer though.

     

    Anyway, if you're only seeing this for a small subset of users, it may be due to network connection issues for those users in particular. I would suggest debugging those network connections if possible, if you haven't already.

     

    Also, Dropbox doesn't enforce a bandwidth quota on these Download calls, and while the Dropbox API does have a rate limiting system, if/when it needs to reject a call it does so by returning an HTTP response with a 429 error code; it wouldn't just return a 200 and then later terminate the connection mid-stream for that.

     

    There may be relevant server timeouts though, e.g., at about one hour in particular. I suggest adding/checking logging on how long exactly these connections take before failing to see if that's why.

     

    Using range requests is a good way to handle/resume incomplete downloads, but note that even when requesting a range, that request would also be subject to any server timeouts, so you may need to continue to monitor that and use multiple range requests, especially on slower connections.

    • pear0533's avatar
      pear0533
      Explorer | Level 3

      There is nothing particularly wrong with the network connections that these users who are experiencing issues have. Plenty of them actually have relatively fast internet, so this shouldn't be an issue. Here are some user reports that I have received to give you an idea of what users themselves are seeing on their sides:

       

      1. "[mod] downloads just stops for no apparent reason PS4 version [a mod available in the launcher is stuck] @54% and XBOX [another mod stuck] @60% connection [to the internet/Dropbox] is stable but shows 0kb send/0kb received after that point [in task manager, presumably]. with occasional spikes"

      2. "My internet maybe isn't fast, but it's definitely [not] slow either. I've been stuck on 35% [while downloading a mod] for over an hour [now]."

      3. "hello, i have good internet but the mod launcher [mod] download is slow and it stays stuck at 43% or 44%, i tried cancelling and redownloading but it keeps stopping at that point."

       

      It seems most of these reports are happening at around the hour mark. Furthermore, I am not making several concurrent calls to the API to retrieve download blobs, I am making a single API call before a mod download even starts, and then reading the resulting stream returned by the call in a loop.

       

      If the server is causing timeouts, is there any way to delay this from happening so that downloads can finish completely? As I said before, single/multiple range requests do not seem to work, and the downloads are much slower using range requests than with the API.

       

      What is even weirder is the fact that downloads using the API for these users who are reporting download stalls are abnormally slow, regardless of whether the user has a poor or strong internet connection. This is what is most concerning, as this would indicate it is actually an issue with the way Dropbox is handling particular network configurations either through the mainline servers or the API network logic itself.

      • Greg-DB's avatar
        Greg-DB
        Icon for Dropbox Staff rankDropbox Staff

        If these are failing at about an hour, it does sound like it is due to the server timeout. There isn't an option to delay or prevent that.

         

        Also "downloads are much slower using range requests than with the API" is a bit unclear as range requests are themselves do use the same API. Specifically, the DownloadAsync method calls the /2/files/download API endpoint, just like you would if you make requests directly to /2/files/download without using the SDK, whether or not you set a range. Range requests just include an additional header to request a specific portion of the file.

         

        In any case, the connection speed to the Dropbox API servers depends on a number of different factors that can change over time or by location, such as the routing between the user's ISP and our servers, and may be slower than the ISP's rated speeds. Sometimes resetting or retrying the connection gets the client a different route and better speeds, but that is outside of our control. Some ISPs also throttle sustained connections so if you see an initial high connection speed followed by lower speeds, that could be the reason.

         

        So, the best recommendation I can offer is to use range requests to continue downloading the rest of the data if a connection fails, and repeat as needed for up to one hour per connection.

About Dropbox API Support & Feedback

Node avatar for Dropbox API Support & Feedback

Find help with the Dropbox API from other developers.

5,877 PostsLatest Activity: 12 months ago
326 Following

If you need more help you can view your support options (expected response time for an email or ticket is 24 hours), or contact us on X or Facebook.

For more info on available support options for your Dropbox plan, see this article.

If you found the answer to your question in this Community thread, please 'like' the post to say thanks and to let us know it was useful!