## Notion document https://www.notion.so/chatwoot/Email-IMAP-Issue-30aa5f274c928062aa6bddc2e5877a63?showMoveTo=true&saveParent=true ## Description PLAIN IMAP channels (non-OAuth) were silently retrying failed authentication every minute, forever. When credentials are wrong/expired, Net::IMAP::NoResponseError was caught and logged but channel.authorization_error! was never called — so the Redis error counter never incremented, reauthorization_required? was never set, and admins were never notified. OAuth channels already had this handled correctly via the Reauthorizable concern. Additionally, Net::IMAP::ResponseParseError (raised by non-RFC-compliant IMAP servers) was falling through to the StandardError catch-all, flooding Estimated impact before fix: ~70–75 broken IMAP inboxes generating ~700k–750k wasted Sidekiq jobs/week. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
44 lines
1.4 KiB
Ruby
44 lines
1.4 KiB
Ruby
require 'rails_helper'
|
|
|
|
shared_examples_for 'backoffable' do
|
|
let(:obj) { FactoryBot.create(described_class.to_s.underscore.tr('/', '_').to_sym) }
|
|
|
|
before do
|
|
allow(GlobalConfigService).to receive(:load).with('BACKOFF_MAX_INTERVAL_MINUTES', 5).and_return(2)
|
|
allow(GlobalConfigService).to receive(:load).with('BACKOFF_MAX_INTERVAL_COUNT', 10).and_return(3)
|
|
# max_interval=2, max_retries=(2-1)+3=4; exhausts on 5th apply_backoff!
|
|
end
|
|
|
|
it 'starts with no backoff' do
|
|
expect(obj.in_backoff?).to be false
|
|
expect(obj.backoff_retry_count).to eq 0
|
|
end
|
|
|
|
it 'ramps backoff on each failure' do
|
|
obj.apply_backoff!
|
|
expect(obj.backoff_retry_count).to eq 1
|
|
expect(obj.in_backoff?).to be true
|
|
end
|
|
|
|
it 'caps wait time at max interval' do
|
|
4.times { obj.apply_backoff! }
|
|
expect(obj.backoff_retry_count).to eq 4
|
|
expect(obj.in_backoff?).to be true
|
|
end
|
|
|
|
it 'exhausts backoff and calls prompt_reauthorization! after max retries' do
|
|
allow(obj).to receive(:prompt_reauthorization!)
|
|
5.times { obj.apply_backoff! }
|
|
expect(obj).to have_received(:prompt_reauthorization!)
|
|
expect(obj.backoff_retry_count).to eq 0
|
|
expect(obj.in_backoff?).to be false
|
|
end
|
|
|
|
it 'clear_backoff! resets retry count and backoff window' do
|
|
obj.apply_backoff!
|
|
obj.clear_backoff!
|
|
expect(obj.in_backoff?).to be false
|
|
expect(obj.backoff_retry_count).to eq 0
|
|
end
|
|
end
|