Skip to content

Conversation

@scottlovegrove
Copy link
Contributor

Summary

Fixes #406 - Resolves the 30-second connection hang issue that was introduced in PR #377 when the library migrated from axios to Node.js native fetch.

Problem

After any API call completes in versions 6.0.0+, the process hangs for approximately 30 seconds before terminating. This also affects Jest tests, causing them to hang even with mocked requests (nock/msw).

Root Cause

Node.js native fetch uses undici under the hood with a global HTTP connection pool that keeps connections alive by default. The default keepAlive timeout is ~30 seconds, causing connections to stay open waiting for potential reuse.

Solution

Configure undici's HTTP Agent with very short keepAlive timeouts (1ms) and pass it to all native fetch calls via the dispatcher option. This ensures connections close immediately after requests complete.

Changes

  • Added undici (^7.16.0) as a dependency
  • Created HTTP agent with minimal keepAlive timeouts in src/utils/fetch-with-retry.ts
  • Configured all native fetch calls to use the custom agent via the dispatcher option

Testing

Manual Testing

  • ✅ Real API calls complete in ~3 seconds (vs ~31 seconds before fix)
  • ✅ All Jest tests pass without timeout issues
  • ✅ No functional changes to API behavior

Test Script for Reviewers

You can verify this fix by running the following test script. Save it as test-connection-hang.js and run it with node test-connection-hang.js <your-api-token>:

/**
 * Test script to verify the connection hang fix for issue #406
 *
 * Before fix: Total execution time ~31s (1s API + 30s hang)
 * After fix: Total execution time ~3s (1s API + 2s verification)
 *
 * Usage: node test-connection-hang.js <your-todoist-api-token>
 */

import { TodoistApi } from './dist/esm/index.js';

const apiToken = process.argv[2];

if (!apiToken) {
  console.error('Error: API token is required');
  console.error('Usage: node test-connection-hang.js <your-todoist-api-token>');
  process.exit(1);
}

console.log('='.repeat(60));
console.log('Connection Hang Test - Issue #406');
console.log('='.repeat(60));
console.log('Start time:', new Date().toISOString());
console.log();

const overallStart = Date.now();

async function main() {
  const api = new TodoistApi(apiToken);

  console.log('[1/3] Making API request to getTasks()...');
  const requestStart = Date.now();

  try {
    const items = await api.getTasks();
    const requestDuration = Date.now() - requestStart;

    console.log(`[2/3] API request completed successfully!`);
    console.log(`      - Tasks fetched: ${items.length}`);
    console.log(`      - Request duration: ${requestDuration}ms`);
    console.log(`      - Completion time: ${new Date().toISOString()}`);
    console.log();

    console.log('[3/3] Waiting 2 seconds to verify no hang...');
    await new Promise(resolve => setTimeout(resolve, 2000));

    const totalDuration = Date.now() - overallStart;

    console.log();
    console.log('='.repeat(60));
    console.log('✓ SUCCESS: No connection hang detected!');
    console.log('='.repeat(60));
    console.log(`Total execution time: ${totalDuration}ms (~${Math.round(totalDuration / 1000)}s)`);
    console.log('End time:', new Date().toISOString());
    console.log();
    console.log('Expected behavior:');
    console.log('  - Before fix: ~31 seconds (includes 30s hang)');
    console.log('  - After fix:  ~3 seconds (no hang)');
    console.log();

    if (totalDuration > 10000) {
      console.log('⚠ WARNING: Execution took longer than expected.');
      console.log('  This might indicate the connection hang issue is present.');
      process.exit(1);
    }

    process.exit(0);

  } catch (error) {
    console.error();
    console.error('✗ ERROR: API request failed');
    console.error('Error details:', error.message);

    if (error.status === 401 || error.status === 403) {
      console.error();
      console.error('Authentication failed. Please check your API token.');
      console.error('You can get your token from: https://todoist.com/app/settings/integrations/developer');
    }

    process.exit(1);
  }
}

main();

How to Reproduce the Bug (Optional)

To see the original bug behavior:

git checkout v6.1.10
npm install && npm run build
node test-connection-hang.js <your-api-token>
# Will hang for ~30 seconds after API call completes

Impact

  • Fixes the reported issue where processes hang for 30 seconds after API calls
  • Resolves Jest test hangs when using mocked requests
  • Restores the immediate completion behavior from v5.9.0
  • No breaking changes or API modifications

🤖 Generated with Claude Code

Fixes #406

This commit resolves the connection hang issue introduced in PR #377 when
the library migrated from axios to Node.js native fetch.

Root cause:
Node.js native fetch uses undici with a global HTTP connection pool that
keeps connections alive by default (~30s timeout). This caused processes
to hang after API calls completed, waiting for connections to close.

Solution:
Configure undici's HTTP Agent with very short keepAlive timeouts (1ms)
and pass it to fetch calls via the dispatcher option. This ensures
connections close immediately after requests complete.

Changes:
- Add undici (^7.16.0) as a dependency
- Create HTTP agent with minimal keepAlive timeouts
- Configure all native fetch calls to use the custom agent

Testing:
- Verified with real API calls: process exits in ~3s vs ~31s before fix
- All Jest tests pass without timeout issues
- No functional changes to API behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@scottlovegrove scottlovegrove requested review from a team and pauloslund and removed request for a team November 24, 2025 18:25
@scottlovegrove scottlovegrove self-assigned this Nov 24, 2025
@scottlovegrove scottlovegrove merged commit 27d6133 into main Nov 24, 2025
2 checks passed
@scottlovegrove scottlovegrove deleted the scottl/sdk-hang branch November 24, 2025 18:26
@garyking
Copy link

garyking commented Nov 24, 2025

It's still broken, unfortunately.

Please see here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6.0.0: Hangs for 30 seconds after it's done

3 participants