Repository navigation
Fix S3/SFTP job contracts, the JSON content type check and dead bulk_root_url - #29
Merged
franciscbalint merged 1 commit intoOct 25, 2026
Merged
Conversation
…root_url
An audit against the 74 documented operations found several places where the
SDK sends something the live API rejects, or reads a failure as a success.
listS3files was going through bulk_get, which puts it under /v2. Every other
S3 and SFTP/FTP call lives on the bulkapi root, so the request 404ed. It now
uses bulk_root_get, and the spec stub that asserted the wrong URL is fixed
with it.
validate_s3_file and score_s3_file took return_url through **options and never
required it, so the live API rejected the job with "This field is required".
They now go through bulk_root_file_job, the helper the other six jobs already
share, rather than rebuilding the parameter hash twice. export_s3_file demanded
export_bucket, but it is optional: omitted, the export goes to the bucket in
the account's settings, the way export_sftp_file's export_path already did.
list_inbox_placement_tests sent pageSize. The API accepts it and ignores it:
page_size=1 paginates, pageSize=1 returns the whole list. It now sends
page_size, bounds it to 1-100, and accepts createdAfter/createdBefore, which
were missing entirely.
The remaining argument checks catch what the API rejects anyway, before the
request: a bulk email finder file needs first_name_column or full_name_column
to have a name to work from, and a new inbox placement test needs a name of at
most 64 characters, a testType of 1, 2 or 3, and a senderEmail when the type
is 1 - that one would otherwise spend inbox-test credits to be told so.
find_email's first_name goes the other way: /guessformat only requires a
domain or a company name, so it is now optional. It stays the first positional
argument, and find_domain and the deprecated guessformat are untouched.
Two things underneath the endpoints:
The content type was compared with == 'application/json', so a response typed
'application/json; charset=utf-8' skipped JSON parsing and came back as a raw
String. An {"error": ...} body then read as a success instead of raising.
__json_content_type__ matches the media type and ignores the parameters.
bulk_root_url / ZEROBOUNCE_BULK_ROOT_URL was written in Configuration and
never read, so a documented-looking environment variable silently did nothing.
It is now the root the S3/SFTP jobs use when it is set. It defaults to nil
instead of the live constant, so leaving it alone still derives the root from
bulk_api_root_url with the /v2 stripped and nobody pointing that at a mock
server loses the derivation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An audit against the 74 documented ZeroBounce operations turned up several places where the SDK sends something the live API rejects, or reads a failure as a success. Each was re-verified against the code before changing.
Wrong URL
list_s3_fileswent throughbulk_get, emittingGET bulkapi/v2/listS3files. Like every other S3 and SFTP/FTP call it lives on the bulkapi root, so the request 404ed. It now usesbulk_root_get.spec/priority3_spec.rbhad a WebMock stub asserting the/v2URL; that stub is fixed in the same commit.Correctness
validate_s3_file/score_s3_filetookreturn_urlthrough**optionsand never required it, so the live API rejected the job with "This field is required". They now go throughbulk_root_file_job— the helper ops 33/34/36-39 already share — instead of rebuilding the parameter hash twice.has_header_rowis now a named keyword (defaultfalse); arbitrary extra options are no longer forwarded, matching the documented field set.export_s3_fileraisedArgumentErrorwhenexport_bucketwas absent. It is optional: omitted, the export goes to the bucket in the account's settings, the wayexport_sftp_file'sexport_pathalready did.list_inbox_placement_testssentpageSize, which the API accepts and then ignores (page_size=1paginates;pageSize=1returns the full list unchanged). It now sendspage_size, bounds it to 1-100 before the request, and acceptscreated_after/created_before(sent ascreatedAfter/createdBefore), which were absent entirely. The spec assertingpageSizeis updated.find_emailrequiredfirst_nameas a positional./guessformatonly requires a domain or a company name, so it is now optional. It stays the first positional argument, so existing calls are unchanged, andfind_domainand the deprecatedguessformatkeep their own signatures.send_email_finder_filenow enforces "first_name_columnORfull_name_columnrequired" — with neither, the finder has no name to build a local part from.create_inbox_placement_testenforcesname≤ 64 characters,test_typein 1|2|3, andsender_emailwhentest_typeis 1. These are rejected by the API anyway, and the last one would otherwise spend inbox-test credits to be told so.Config / robustness
Configuration#bulk_root_url/ZEROBOUNCE_BULK_ROOT_URLwas dead config — written, never read — so a documented-looking environment variable silently did nothing. It is now wired throughBaseRequest.bulk_api_root_without_v2and is the root the S3/SFTP jobs use when set. It defaults tonilrather than the live constant, so leaving it alone still derives the root frombulk_api_root_urlwith the/v2stripped: nobody pointingbulk_api_root_urlat a mock server loses that derivation.== 'application/json', so a response typedapplication/json; charset=utf-8skipped JSON parsing and the raw body came back as aString. An{"error": ...}payload then read as a success instead of raising.BaseRequest.__json_content_type__matches the media type and ignores the parameters after the semicolon. Applied in bothRequestandMockRequest; affects ops 12/14/16-19/24-27/30.Tests
spec/priority3_spec.rbis expanded from 3 examples to a full offline suite covering request method, full URL, parameters, auth placement (query string vs. form body vs. multipart) and response parsing including error bodies, for every changed method.spec/bulk_jobs_spec.rbgains the Inbox Placement Tester bounds, andspec/base_request_spec.rbthe content-type helper. The live suite'sreturn_urlcheck now coversvalidate_s3_fileandscore_s3_filetoo. No API key is written to any file, fixture or commit.Baseline before the change was 173 examples, 0 failures.
Note: RuboCop had to be run outside
bundle exec— RuboCop 1.49 cannot boot under this machine's Ruby 4.0 (cannot load such file -- benchmark). The new specs are folded into the existingspec/priority3_spec.rbrather than a new file, so nothing had to be added to.rubocop_todo.yml, which.rubocop.ymlasks us not to grow.The live suite (
ruby -Ilib live/live_api.rb) was not run — it needs real API keys, which this environment does not have. AGENTS.md requires it by hand before release.🤖 Generated with Claude Code