Кожна інтеграція тікетів доставляє двічі. Будуйте її з розрахунком на це.
Webhooks повторюють доставку, партнери надсилають знову, мережі гублять відповіді. Як побудувати міст між системами тікетів з підписом, ідемпотентний, без циклів і з обмеженими правами, з кодом, який можна використати повторно.

З’єднати вашу систему тікетів із порталом партнера виглядає як робота на один вечір: надіслати webhook, коли щось змінюється, викликати їхній API, коли вони відповідають. Перше демо завжди працює. Проблеми починаються на третьому тижні, коли timeout змушує відправника повторити спробу, тікет отримує ту саму відповідь двічі, а дві системи починають відповідати одна одній по колу о третій ночі. Ця стаття про чотири властивості, які це зупиняють, хоч би які платформи тікетів стояли з обох кінців.
Виходьте з доставки at-least-once
Будь-який відправник, що повторює спроби, може доставити ту саму подію більше ніж один раз. Вибору в нього немає: якщо ваш сервер обробив запит, але відповідь загубилася на зворотному шляху, відправник бачить timeout і мусить спробувати знову. «Exactly once» у мережі не існує. Існує доставка at-least-once плюс отримувач, який може безпечно побачити те саме двічі.
Отже, перше правило дизайну: кожна подія несе стабільний, унікальний ідентифікатор, який обирає відправник, а отримувач записує ідентифікатори, які вже застосував.
CREATE TABLE applied_events (
event_id TEXT PRIMARY KEY,
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ticket_ref TEXT
);
При кожній доставці спочатку спробуйте вставити ID. Якщо вставка не проходить через первинний ключ, цю роботу ви вже зробили: відповідайте успіхом і зупиняйтеся. Відповідь успіхом має значення. Якщо ви відповісте помилкою, відправник і далі повторюватиме те, що ви вже застосували.
ID події має належати події, а не спробі доставки. Відповідь у тікеті 4711 має нести ID, похідний від цієї відповіді, наприклад ID транзакції у вихідній системі, щоб кожна її повторна доставка несла те саме значення.
Підписуйте кожну доставку і перевіряйте годинник
Підписуйте кожну доставку: HMAC від сирого тіла зі спільним секретом, надісланий у заголовку, разом із міткою часу, щоб перехоплений запит не можна було відтворити наступного тижня.
import hmac, hashlib, time
SECRET = load_secret() # never in the code repository
MAX_SKEW = 300 # seconds
def verify(raw_body: bytes, ts_header: str, sig_header: str) -> bool:
ts = int(ts_header)
if abs(time.time()-ts) > MAX_SKEW:
return False
signed = f"{ts}.".encode() + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_header)
Три деталі, у яких помиляються. Підписуйте сирі байти, а не повторно серіалізований об’єкт JSON, бо дві бібліотеки не серіалізують той самий об’єкт однаково. Порівнюйте функцією зі сталим часом виконання, а не ==. І тримайте окремий секрет для кожного партнера, щоб можна було ротувати один, не чіпаючи інших.
Розривайте цикли навмисно
Міст копіює відповіді в обидва боки. Якщо система A пересилає свої відповіді в B, а B пересилає свої відповіді в A, то відповідь, яка прийшла з B, з погляду A є новою відповіддю в тікеті. Без запобіжника вона йде прямо назад. Потім те саме робить B.
Виправлення: позначати походження і перевіряти його перед пересиланням.
- Коли ви застосовуєте вхідну відповідь, записуйте, що вона прийшла від партнера, наприклад у custom field або в атрибуті транзакції.
- Ваш вихідний тригер пропускає будь-яку транзакцію, позначену як partner-originated.
- Зробіть те саме на боці партнера; не покладайтеся лише на один кінець.
Потім перевірте це на справжньому тікеті: надішліть відповідь з кожного боку і переконайтеся, що кожна з’являється на іншому боці рівно один раз.
Дайте партнеру одну чергу і нічого більше
Вхідний напрямок зазвичай означає, що партнер викликає API вашої системи тікетів. Дайте йому окремий обліковий запис, ніколи не спільний адміністраторський токен, і надайте права лише на одну чергу.
У Request Tracker це означає користувача без глобальних прав, а далі лише на спільній черзі лише ті права, які потрібні мосту: зазвичай SeeQueue, ShowTicket, CreateTicket, ReplyToTicket і ModifyTicket. Не давайте ShowTicketComments, щоб партнер ніколи не міг читати внутрішні коментарі, і не давайте нічого на жодній іншій черзі. Випустіть auth token для цього користувача і передайте його партнеру.
Потім доведіть межі замість того, щоб довіряти їм:
# Should succeed: the shared queue
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: token $PARTNER_TOKEN" \
https://rt.example.com/REST/2.0/queue/Partner-Shared
# Should be refused: any other queue
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: token $PARTNER_TOKEN" \
https://rt.example.com/REST/2.0/queue/General
Одна порада саме щодо Request Tracker: якщо RT працює під Apache з mod_fcgid, заголовок Authorization не передається застосунку, доки ви не додасте FcgidPassHeader Authorization у virtual host. Запити з токеном тоді провалюються як неавтентифіковані, хоча токен дійсний. Варто знати це до того, як витратите на це вечір.
Тримайте коментарі поза мостом
Пересилайте відповіді; ніколи не пересилайте коментарі. Вашим інженерам потрібне місце, де можна написати «цей клієнт запитує це вже втретє» так, щоб це не з’явилося в чужому порталі. Перевірте це коментарем з очевидним маркером і пошукайте його на боці партнера.
Ніколи не губіть нічого мовчки
Коли endpoint партнера недоступний, ваші вихідні доставки мають чекати, а не зникати:
- Повторюйте з експоненційним backoff і jitter, обмежений час.
- Після цього паркуйте подію у dead-letter store разом із помилкою.
- Переглядайте dead-letter store за розкладом і повторюйте.
- Сповіщайте людину, якщо щось лишається запаркованим довше, ніж ви можете собі дозволити.
- Логуйте кожну доставку з ID події, тікетом, кодом статусу і тривалістю.
Виконуйте доставку у worker, а не всередині власного запиту системи тікетів. Якщо партнер повільний, ваш персонал не повинен чекати, доки збережеться сторінка.
Ще дві захисні звички. Під час тестування перевіряйте вкладення за контрольними сумами з обох боків; «виглядає так само» не є тестом. І обмежуйте розмір того, що приймаєте: вбудоване зображення, вставлене в тікет у повній роздільності, може бути в багато разів більшим за текст навколо нього. Обрізайте його або зберігайте окремо на вході, замість того щоб дозволити одному важкому тікету сповільнювати кожну сторінку, яка його рендерить.
Чекліст для вашого власного моста
- Кожна подія має ID, обраний відправником, а отримувач записує застосовані ID.
- Кожен запит підписаний по сирому тілу, з міткою часу і секретом на партнера.
- Зміни, що походять від партнера, позначаються і ніколи не надсилаються назад.
- Токен партнера працює на одній черзі і відхиляється на будь-якій іншій, що доведено тестом.
- Коментарі ніколи не перетинають міст; відповіді перетинають завжди.
- Існують повтори, dead letters, періодичні перегляди і сповіщення, а кожна доставка логується.
- Вкладення перевірені за контрольними сумами; завеликий вміст обробляється на вході.
Ми будуємо й експлуатуємо такі мости між Request Tracker і порталами партнерів. Якщо ви плануєте такий або живете з таким, що поводиться погано, ми радо допоможемо.
Related
Пов’язане: Managed Request Tracker з мостами до партнерів · Managed team chat на Mattermost
Опубліковано 9 жовтня 2026 р..
Усі нотатки з практики