Skip to content

[BugFix] Return 400 instead of 500 on vectorSearch() argument-count mismatch#5589

Open
mengweieric wants to merge 1 commit into
opensearch-project:mainfrom
mengweieric:fix/r39-vectorsearch-dup-arg-500
Open

[BugFix] Return 400 instead of 500 on vectorSearch() argument-count mismatch#5589
mengweieric wants to merge 1 commit into
opensearch-project:mainfrom
mengweieric:fix/r39-vectorsearch-dup-arg-500

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Description

A vectorSearch() table-function call with more arguments than its resolved signature declares crashes with an unchecked IndexOutOfBoundsException that surfaces as HTTP 500 instead of a clean 400. The simplest trigger is a duplicate named argument:

SELECT v.id FROM vectorSearch(table='idx', table='idx', field='embedding', vector='[1.0,1.0]', option='k=3') AS v

returns:

{"error":{"reason":"There was internal problem at backend","details":"Index 4 out of bounds for length 4","type":"ArrayIndexOutOfBoundsException"},"status":500}

Root cause

VectorSearchTableFunctionResolver.resolve() returns a fixed-arity resolved signature (four STRING params) regardless of how many arguments it was called with. In BuiltinFunctionRepository.resolve(), when the supplied argument list is longer than the resolved type list, the call falls through to castArguments, whose loop iterates over arguments.size() while indexing into the shorter targetTypes list. With five arguments and four declared types it reads targetTypes.get(4) and throws. The resolver's own arity and duplicate-name validation lives inside the builder lambda and never runs because the crash happens earlier, during cast-wrapper construction.

Fix

Guard the argument count against the resolved signature in castArguments and throw an ExpressionEvaluationException (which maps to a clean 400) before any indexing. The fix is at the generic repository layer, so it protects every custom FunctionResolver, not only vectorSearch().

Testing

  • :core:test and :opensearch:test pass.
  • Added unit tests: BuiltinFunctionRepositoryTest.resolve_should_not_index_past_resolved_signature (reproduces the crash at the root-cause layer) and VectorSearchTableFunctionResolverTest.resolve_rejectsDuplicateNamedArgumentExceedingArity (the five-argument duplicate shape).
  • Added VectorSearchIT.testDuplicateNamedArgumentRejectsWith400 asserting the request returns 400, not 500. NOTE: this integration test could not be executed locally because main currently has an unrelated pre-existing compile error in the plugin module (org.opensearch.analytics.QueryRequestContext constructor mismatch in RestUnifiedQueryAction) that blocks :integ-test:integTest. CI should exercise it.
  • spotlessJavaCheck passes on core, opensearch, and integ-test.

Check List

  • New functionality includes testing.
  • New functionality has been documented.
  • Commits are signed per the DCO using --signoff.

Draft pending local IT verification once the unrelated main compile break is resolved.

@mengweieric mengweieric added bug Something isn't working skip-changelog skip-diff-analyzer Maintainer to skip code-diff-analyzer check, after reviewing issues in AI analysis. skip-diff-reviewer Maintainer to skip code-diff-reviewer check, after reviewing issues in AI analysis. SQL bugFix labels Jun 26, 2026
@mengweieric mengweieric force-pushed the fix/r39-vectorsearch-dup-arg-500 branch 2 times, most recently from 46c49fd to 3b1f8de Compare June 26, 2026 13:58
@mengweieric mengweieric marked this pull request as ready for review June 26, 2026 13:58
A vectorSearch() call with more arguments than its resolved signature
declares (for example a duplicate named argument such as
table='x', table='x') crashed with an unchecked IndexOutOfBoundsException
surfaced as HTTP 500. The resolver returns a fixed-arity signature
regardless of how many arguments it was called with, so castArguments
looped over the supplied arguments while indexing into the shorter
resolved-type list and ran off the end.

Guard the argument count against the resolved signature in
castArguments and throw an ExpressionEvaluationException, which maps to a
clean 400, before any indexing. This protects every custom function
resolver, not only vectorSearch().

Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
@mengweieric mengweieric force-pushed the fix/r39-vectorsearch-dup-arg-500 branch from 3b1f8de to 5dda148 Compare June 26, 2026 15:04
// surfaces as HTTP 500. Reject that overflow with a clean ExpressionEvaluationException.
// Under-arity is left to the resolver's own validation, which produces a more specific
// message; it cannot overflow the resolved type list.
if (arguments.size() > targetTypes.size()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> and not !=?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is method is very generic. So the changes here will fix all such mismatch argument case for similar functions?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that's intended, though the real scope is narrower than it looks. Normal functions never reach this path, since DefaultFunctionResolver rejects an arity mismatch at match() first. It only matters for custom resolvers that hand roll resolve() to return a fixed arity signature without size matching the input, like the table function resolvers (vectorSearch, and the Prometheus ones follow the same shape). So it covers all fixed arity custom resolvers, which is why I put the guard here rather than only in vectorSearch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, > is on purpose. The crash only happens on overflow, when there are more args than the resolved signature, so the cast loop runs off the end of targetTypes. Fewer args can't overflow, and under arity is already caught upstream anyway: normal functions reject it at the resolver via match() returning NOT_MATCH, and vectorSearch catches it in validateArguments with the specific "requires 4 arguments (table, field, vector, option)" message. So != would just shadow those two better messages with this generic one. One side effect worth noting: over vs under arity now produce slightly different wording, both 400s, so no functional difference.

// Under-arity is left to the resolver's own validation, which produces a more specific
// message; it cannot overflow the resolved type list.
if (arguments.size() > targetTypes.size()) {
throw new ExpressionEvaluationException(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be IllegalArgumentException or Syntax exception?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExpressionEvaluationException is the right one here. In JdbcResponseFormatter the status comes out as (SyntaxCheckException || QueryEngineException) ? 400 : 503, and ExpressionEvaluationException extends QueryEngineException, so it maps to 400. IllegalArgumentException is neither, so it would land on the 503 branch and bring back a 5xx for bad input. It's also what the vectorSearch resolver already throws for its other validations, so this keeps it consistent.

// surfaces as HTTP 500. Reject that overflow with a clean ExpressionEvaluationException.
// Under-arity is left to the resolver's own validation, which produces a more specific
// message; it cannot overflow the resolved type list.
if (arguments.size() > targetTypes.size()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is method is very generic. So the changes here will fix all such mismatch argument case for similar functions?

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

Labels

bug Something isn't working bugFix skip-changelog skip-diff-analyzer Maintainer to skip code-diff-analyzer check, after reviewing issues in AI analysis. skip-diff-reviewer Maintainer to skip code-diff-reviewer check, after reviewing issues in AI analysis. SQL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants