Усі статті
Deep dive8 хв читання

І Apple, і Google можуть доставити те саме повернення на ваш сервер більше одного разу, а дубльовані сповіщення про повернення коштують вам грошей, якщо ви реагуєте на кожне з них

Apple повторює сповіщення про повернення до п'яти разів, а Google Play спирається на Pub/Sub із доставкою at-least-once, тож те саме повернення може дійти до вашого сервера більше одного разу. Ось як обробляти дубльовані сповіщення про повернення, не віднімаючи баланс двічі й не спалюючи квоту API вдвічі.

Багато однакових паперових конвертів, складених на темному столі, один відсунуто вбік, як образ дубльованих сповіщень про повернення, що надходять на ваш сервер

Головне

  • Apple повторює App Store Server Notification V2 п'ять разів, через 1, 12, 24, 48 і 72 години після останньої спроби, щоразу, коли ваш сервер не відповідає статусом HTTP між 200 і 206. Рахуючи першу спробу, одне повернення може надійти до шести разів.
  • Кожне справжнє повторення Apple несе той самий notificationUUID, тож саме це поле, а не ідентифікатор транзакції, є вашим ключем дедуплікації.
  • Real-time Developer Notifications від Google Play спираються на Cloud Pub/Sub, який гарантує доставку at-least-once і жодного порядку, тож те саме повідомлення може надійти двічі або в неправильному порядку. Google каже вам перевіряти унікальність messageId, перш ніж щось обробляти.
  • Періодичні сповіщення CONSUMPTION_REQUEST від Apple не є повтореннями. Apple продовжує надсилати нові протягом усього відкритого вікна повернення, кожне з іншим notificationUUID, тож дедуплікація за notificationUUID правильно зберігає кожне з них.
  • Той самий transactionId Apple може нести більше одного рішення, наприклад REFUND_DECLINED, за яким пізніше йде REFUND, тож дедуплікація лише за ідентифікатором транзакції відкидає окрему подію, яка вам була потрібна.
  • Відхилення дубліката поверненням 4xx або 5xx лише змушує магазин повторити його. Виконуйте дедуплікацію у власній базі даних і завжди повертайте статус успіху.
  • Обробник повернень, який не є ідемпотентним, спрацьовує двічі під час другої доставки. Він віднімає баланс двічі, скасовує виплату двічі або спалює платну квоту Play Developer API та App Store Server API, повторно перевіряючи повернення, яке вже закрив.

Ваш сервер отримає ту саму подію повернення більше одного разу, і обидва магазини спроектували це навмисно. Apple повторює App Store Server Notification до п'яти разів, коли ваша кінцева точка не відповідає чисто. Google Play доставляє свої Real-time Developer Notifications через Cloud Pub/Sub, який обіцяє доставку at-least-once і нічого про порядок. Тож питання ніколи не в тому, чи надійде дублікат. Воно в тому, що робить ваш код удруге, коли бачить те саме повернення. Зробіть це неправильно, і ви віднімете баланс двічі, скасуєте виплату двічі або спалите платну квоту API, повторно перевіряючи повернення, яке вже закрили. Ось як дубльовані сповіщення про повернення насправді доходять до вас, які повтори є справжніми дублікатами, а які лише такими виглядають, і як їх обробляти, щоб друга доставка була безкоштовною.

Сповіщення про повернення доставляється принаймні один раз, що не те саме, що рівно один раз

Обидва магазини трактують доставлене сповіщення як обіцянку, яку вони знову й знову намагаються виконати, а не як єдиний постріл, який вони роблять і забувають. Це добре для надійності, бо сповіщення, яке ви пропустили під час розгортання, усе одно дійде до вас пізніше. Це пастка для коректності, бо механізм, який гарантує, що ви зрештою отримаєте подію, гарантує також, що іноді ви отримаєте її двічі. Ваш обробник має бути ідемпотентним, тобто друга й третя доставки одного повернення не змінюють нічого, чого перша вже не змінила.

Apple повторює п'ять разів протягом трьох днів

Коли Apple надсилає App Store Server Notification V2, вона очікує, що ваш сервер відповість статусом HTTP у діапазоні від 200 до 206. Будь-що інше, 4xx або 5xx, каже Apple, що доставка не вдалася, і Apple повторює. Розклад фіксований: п'ять повторень, через 1, 12, 24, 48 і 72 години після попередньої спроби. Рахуючи першу спробу, одна подія повернення може надійти до шести разів, розтягнута приблизно на тиждень. Кожне з цих повторень несе той самий notificationUUID. Це поле є вашим ключем дедуплікації. Якщо ви вже записали notificationUUID, доставка, яку ви тримаєте, є повтором, і правильна відповідь, це не зберігати нічого нового й усе одно повернути 200.

Google Play спирається на Pub/Sub, який обіцяє at-least-once і нічого не каже про порядок

Real-time Developer Notifications від Google Play публікуються до теми Cloud Pub/Sub. Гарантія доставки Pub/Sub, це at-least-once, і жодної гарантії порядку взагалі немає. Це означає, що те саме повідомлення може бути доставлене до вашої кінцевої точки більше одного разу, а два повідомлення для однієї покупки можуть надійти в неправильному порядку. Власна настанова Google однозначна: розпакуйте поле data у base64, прочитайте messageId і перевірте, що ви його ще не бачили, перш ніж щось обробляти. Дубльований messageId, це повтор, який ви пропускаєте. Два різних сповіщення про одну покупку все одно мають потрапити в той самий запис, тож прив'яжіть свій збережений стан також до purchaseToken і дозвольте пізнішій події оновити рядок, який створила ранішня.

ПлатформаМодель доставкиДедуплікація заСигнал успіхуЯкщо ви не підтверджуєте
App Store Server Notifications V2До 6 спроб: перша плюс 5 повторень через 1, 12, 24, 48, 72 годиниnotificationUUIDHTTP від 200 до 206Apple повторює за фіксованим розкладом, потім зупиняється
Google Play RTDN через Pub/SubAt-least-once, без гарантії порядкуPub/Sub messageId, прив'язано до сутності за purchaseTokenHTTP 200 на push або явне ackPub/Sub надсилає повторно після спливання дедлайну ack

Повтори, які не є дублікатами

Не кожне сповіщення, яке виглядає як уже бачене, є повторенням. Дві поведінки Apple надсилають справді нові події, що поділяють одну покупку, але кожну треба обробити, а їхнє злиття наївною дедуплікацією відкидає інформацію, яка вам була потрібна.

Apple надсилає свіжі CONSUMPTION_REQUEST, а не повторення

Під час відкритого запиту на повернення за витратний товар Apple не надсилає один CONSUMPTION_REQUEST і не чекає. Вона надсилає нові періодично протягом усього відкритого вікна повернення, доки повернення не буде закрито. Співробітники Apple підтвердили, що це не повторення, а промовистою ознакою є поле, за яким ви виконуєте дедуплікацію: кожен свіжий CONSUMPTION_REQUEST несе інший notificationUUID. Тож дедуплікація, прив'язана до notificationUUID, автоматично робить правильну річ. Вона зливає справжні повторення й зберігає кожен окремий запит. Чого вам не можна робити, це виконувати дедуплікацію за ідентифікатором транзакції та типом сповіщення, бо це змусило б замовкнути кожен CONSUMPTION_REQUEST після першого й коштувало б вам 12-годинного вікна доказів на тих, що відкинули.

Одна транзакція може нести більше одного рішення

Один transactionId може за своє життя породити більше одного результату повернення. Apple може надіслати REFUND_DECLINED, а потім, пізніше, REFUND для тієї самої транзакції, і розробники повідомляють про отримання трьох або більше сповіщень, пов'язаних із поверненням, для одного ідентифікатора транзакції. Кожне є окремою подією з власним notificationUUID. Якщо вашим ключем дедуплікації є ідентифікатор транзакції, друге рішення виглядає як дублікат першого, і ви ніколи не дізнаєтеся, що повернення зрештою було надано. Ідентифікатор транзакції групує події. Він їх не ідентифікує.

Роботизований захват піднімає одну дубльовану посилку з конвеєрної лінії у бічний контейнер, образ, що символізує дедуплікацію повторюваних сповіщень про повернення

Скільки насправді коштує вам дублікат

Сповіщення про повернення, це не індикатор стану. Воно запускає справжні дії: ви відкликаєте доступ, віднімаєте баланс витратного товару, скасовуєте виплату автору, викликаєте App Store Server API або Play Developer API, щоб підтвердити стан. Виконайте будь-яку з них удруге на дублікаті, і витрати реальні.

Простежте гроші. Двічі відкликати доступ нешкідливо, бо доступ уже зник. Двічі відняти баланс, ні: користувача, який купив набір монет і повернув його, можна загнати в негативний баланс, який ваша команда підтримки потім має розплутувати вручну. Двічі скасувати виплату забирає назад гроші, які ви вже раз повернули, і тепер ви винні автору вибачення й виправлення. А кожен дублікат, який ви повторно обробляєте проти API магазину, витрачає квоту, яку Google прямо радить вам берегти, тож сплеск повторних доставок Pub/Sub під час збою може заштовхнути вас в обмеження швидкості саме того дня, коли ви найменше можете собі це дозволити.

Вікна доказів повернення підвищують ставки з боку Apple. Якщо наївна дедуплікація змушує замовкнути повторювані CONSUMPTION_REQUEST, які Apple надсилає протягом відкритого вікна повернення, ви можете пропустити те, на яке треба було відповісти, а CONSUMPTION_REQUEST, на який ви не відповідаєте протягом 12 годин, це повернення, яке Apple часто надає за замовчуванням. Це не подвійне списання. Це втрачений продаж плюс обчислення, виклики API, сховище й виплати, які ви вже витратили на доставлення покупки, жодне з яких повернення не повертає.

Режим збоюЩо йде не такСкільки коштує
Повторне віднімання балансу на дубльованому REFUNDБаланс витратного товару користувача стає негативнимРучний час підтримки на звірку та поганий клієнтський досвід
Двічі скасувати виплатуВи забираєте назад гроші, які вже раз повернулиВиправлення для автора та розчищення бухгалтерії
Повторна обробка проти API магазинуДубльовані виклики спалюють квоту Play Developer або App Store Server APIОбмеження швидкості під час збою, що спричинив повторну доставку
Надмірна дедуплікація CONSUMPTION_REQUESTВи відкидаєте окремий запит на повернення як хибний дублікатПропущене 12-годинне вікно, тож Apple надає повернення за замовчуванням

Як обробляти дубльовані сповіщення про повернення без подвійної дії

Шаблон однаковий в обох магазинах, з іншим ключем. Запишіть доставку, перевірте ключ, перш ніж діяти, дійте один раз і завжди скажіть магазину, що ви її отримали.

  • Виконуйте дедуплікацію за ідентифікатором доставки магазину, а не за транзакцією. Використовуйте notificationUUID для Apple і Pub/Sub messageId для Google Play. Зберігайте його з обмеженням унікальності, щоб одночасний дублікат програв перегони замість подвійної дії.
  • Зробіть дію нижче за течією ідемпотентною за її власними правилами. Прив'язка до ідентифікатора доставки зупиняє повторну обробку, але напишіть також ефект так, щоб відкликання, віднімання чи скасування спершу перевіряло поточний стан і було безпечним для дворазового запуску.
  • Спершу зберігайте, потім підтверджуйте. Запишіть подію до вашої бази даних, перш ніж повернути 200 або підтвердити повідомлення Pub/Sub. Якщо ви підтвердите спершу, а запис не вдасться, магазин уважатиме повідомлення доставленим і ніколи не надішле його знову, і тепер ви втратили його назавжди.
  • Завжди повертайте статус успіху, навіть для дубліката. 200 до 206 для Apple, 200 на push Pub/Sub для Google. Відхилення повтору помилкою лише змушує магазин надіслати його знову.
  • Групуйте за сутністю, ідентифікуйте за подією. Прив'яжіть свій збережений стан покупки до purchaseToken або originalTransactionId, щоб доставки в неправильному порядку оновлювали один рядок, але трактуйте кожен notificationUUID чи messageId як власну подію, бо одна покупка законно породжує кілька.

Короткий контрольний список, перш ніж довіряти своєму вебхуку повернень

  • Доставки Apple дедупліковані за notificationUUID, і повтор не записує нічого нового, але все одно повертає 200.
  • Доставки Google Play дедупліковані за Pub/Sub messageId, перевіреним перед будь-якою обробкою.
  • Стан покупки прив'язаний до purchaseToken або originalTransactionId, тож події в неправильному порядку потрапляють в один запис.
  • Кожен побічний ефект повернення, відкликання, віднімання чи скасування, безпечний для запуску більше одного разу.
  • Ваш обробник записує подію, перш ніж її підтвердити, ніколи після.
  • Повторювані CONSUMPTION_REQUEST трактуються як окремі запити, а не дублікати, тож жодне відкрите вікно повернення не відкидається.

Пропустіть дублікат через власний вебхук навмисно й подивіться, як удруге він нічого не змінює. Це весь тест. Обробник повернень, який безпечно вдарити двічі, це той, про який ви перестаєте хвилюватися тієї миті, коли магазин вирішує вдарити його шість разів.

Поширені запитання

Чому мій сервер отримує те саме сповіщення про повернення з App Store більше одного разу?
Тому що Apple повторює App Store Server Notification V2 до п'яти разів, через 1, 12, 24, 48 і 72 години після останньої спроби, щоразу, коли ваш сервер не відповідає статусом HTTP між 200 і 206. Кожне повторення несе той самий notificationUUID, тож ви можете його розпізнати й пропустити.
Яке поле мені використовувати для дедуплікації App Store Server Notifications?
Використовуйте notificationUUID. Справжнє повторення завжди повторює той самий notificationUUID, тоді як кожна справді нова подія, зокрема кожен свіжий CONSUMPTION_REQUEST, отримує інший, тож дедуплікація за notificationUUID пропускає повтори, не відкидаючи окремих подій.
Чи повторювані сповіщення CONSUMPTION_REQUEST є дублікатами, які мені слід ігнорувати?
Ні. Apple надсилає нові сповіщення CONSUMPTION_REQUEST періодично протягом відкритого вікна повернення, і співробітники Apple підтверджують, що це не повторення. Кожне має власний notificationUUID, тож обробіть кожне з них. Їх відкидання ризикує пропустити 12-годинне вікно, яке Apple дає вам на відповідь.
Як мені виконувати дедуплікацію Google Play Real-time Developer Notifications?
Прочитайте Pub/Sub messageId з кожного сповіщення й перевірте його проти вже оброблених, перш ніж діяти, бо Pub/Sub доставляє at-least-once і може надіслати те саме повідомлення більше одного разу. Google прямо радить це, щоб уникнути подвійної обробки та змарнованої квоти API.
Чи слід мені повертати помилку, щоб відхилити дубльоване сповіщення про повернення?
Ні. Повернення 4xx або 5xx каже магазину, що доставка не вдалася, тож він усе одно повторює. Виконуйте дедуплікацію у власній базі даних і завжди повертайте статус успіху, HTTP 200 до 206 для Apple або підтвердження 200 для push Google Play.

Джерела та додаткове читання

RefundHalt

Автопілот повернень для App Store і Google Play

Читайте далі

Deep dive8 хв читання

Повернення коштів у Family Sharing скасовує один платіж, але може лишити ще п'ятьох людей, які й далі користуються вашим застосунком, і тільки ваш сервер здатен їх відключити

Повернення коштів у Family Sharing скасовує один платіж, але може лишити до п'яти членів родини на ваших платних функціях. Apple надсилає REVOKE і очікує, що ваш сервер завершить доступ. Ось як працює повернення коштів зі спільним доступом у родині і скільки коштує одне таке повернення.

Playbook8 хв читання

Обробка повернень ламається непомітно, тому протестуйте повернення за вбудовані покупки в sandbox раніше, ніж це зробить реальний клієнт

Ваша обробка повернень запускається лише після того, як клієнт уже пішов, тому помилка в ній лишається невидимою, поки не почне коштувати реальних грошей. Обидва магазини дозволяють спершу запустити повернення в тестовому середовищі. Ось як протестувати повернення за вбудовані покупки в App Store і Google Play до того, як повернення стане реальним.

Наступний запит на повернення вже в дорозі.

Налаштуйте RefundHalt за час, потрібний, щоб прочитати черговий лист у підтримку про повернення, яке ви не встигли оскаржити.