Skip to content

Node.js SDK rejects empty first-page pagination cursors #44264

Open

Description

@Winoren

Self Checks

  • I have read the contributing guide and language policy.
  • I searched existing issues and pull requests for Node.js pagination and empty cursors.
  • This report is in English and includes a local reproduction.

Dify version

Node.js SDK dify-client 3.1.0 from main 8857292ad7b66d558493c3179a2049fd35ddbf9e, Node.js 25.9.0.

Cloud or Self Hosted

Self Hosted (Source). The SDK reproduction uses a loopback HTTP fixture, without a deployed Dify server or model calls.

Steps to reproduce

First-page pagination state is commonly initialized to an empty string. Dify's Service API accepts empty cursor strings: MessageListQuery.first_id, ConversationListQuery.last_id, and ConversationVariablesQuery.last_id all use UUIDStrOrEmpty | None; their handlers normalize an empty cursor to None. The Node.js SDK rejects this value before sending the request.

Build sdks/nodejs-client with vp pack, save the following as repro.mjs in that directory, and run node repro.mjs:

import assert from 'node:assert/strict'
import { createServer } from 'node:http'
import { ChatClient } from './dist/index.js'
const urls = []
const server = createServer((request, response) => {
  urls.push(request.url)
  response.writeHead(200, { 'content-type': 'application/json' })
  response.end('{"data":[]}')
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
try {
  const client = new ChatClient({ apiKey: 'fixture', baseUrl: `http://127.0.0.1:${server.address().port}`, maxRetries: 0 })
  const calls = [
    ['conversations', () => client.getConversations('fixture', '', 10)],
    ['messages', () => client.getConversationMessages('fixture', '00000000-0000-0000-0000-000000000001', '', 10)],
    ['variables', () => client.getConversationVariables('00000000-0000-0000-0000-000000000001', 'fixture', '', 10)],
  ]
  const results = []
  for (const [name, call] of calls) {
    try { await call(); results.push({ name, result: 'sent' }) }
    catch (error) { results.push({ name, result: error.name, message: error.message }) }
  }
  console.log(JSON.stringify({ results, urls }))
  assert.equal(urls.length, 3)
  for (const raw of urls) {
    const url = new URL(raw, 'http://localhost')
    assert.equal(url.searchParams.has('first_id'), false)
    assert.equal(url.searchParams.has('last_id'), false)
    assert.equal(url.searchParams.get('limit'), '10')
  }
} finally {
  server.closeAllConnections()
  await new Promise(resolve => server.close(resolve))
}

Expected Behavior

All three calls request their first page, omitting an empty cursor just as when the argument is omitted or null. Existing non-empty cursors and request limits remain unchanged.

Actual Behavior

All three calls throw ValidationError (lastId/firstId must be a non-empty string when set), and the HTTP fixture receives zero requests.

The method-level ensureOptionalString rejects '' even though the endpoint schemas explicitly support it. This is distinct from #44097 (keyword filters), #44263 (SSE framing), and #44199 (streamed errors).

Verification boundary

I verified the API contract in the current backend query schemas and handlers, and reproduced the SDK's outgoing requests with the local fixture. This is not a deployed-backend or database integration test. A focused fix and tests are prepared; the three empty-cursor regressions fail on unchanged SDK production code and pass with the fix. Whitespace-only cursor validation remains strict.

From Codex

Activity

  1. dosubot commented on Oct 17, 2026

    @dosubot

    One additional detail: getConversationVariables needs empty-cursor omission as well as validation handling. It builds last_id with lastId ?? undefined, and the HTTP serializer preserves ''. By contrast, conversations and messages already omit falsy cursors when building their queries. Simply allowing '' through validation would therefore still send last_id= for variables. [1] [2] [3]

    As a caller-side workaround, normalize only the exact empty string to undefined before passing any of the three cursor arguments:

    const cursorArg = cursor === '' ? undefined : cursor

    That uses the existing omitted-cursor path without changing non-empty values or masking whitespace-only validation errors. All three methods accept undefined, and the shared validator skips it while rejecting whitespace-only strings. [1] [2] [4]

    To reply, just mention @dosu.


    Docs are dead. Just use Dosu.

    Leave Feedback Ask Dosu about dify

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    review: mediumghfind author score; see https://ghfind.com

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions