@@ -63,6 +63,32 @@ function extractStatusCode(err: unknown): number | null {
6363 return null ;
6464}
6565
66+ function extractErrorCode ( err : unknown ) : string | null {
67+ if ( ! isRecord ( err ) ) {
68+ return null ;
69+ }
70+
71+ const direct = err . code ;
72+ if ( typeof direct === "string" && direct . trim ( ) ) {
73+ return direct ;
74+ }
75+
76+ const response = err . response ;
77+ if ( ! isRecord ( response ) ) {
78+ return null ;
79+ }
80+
81+ const body = response . body ;
82+ if ( isRecord ( body ) ) {
83+ const error = body . error ;
84+ if ( isRecord ( error ) && typeof error . code === "string" && error . code . trim ( ) ) {
85+ return error . code ;
86+ }
87+ }
88+
89+ return null ;
90+ }
91+
6692function extractRetryAfterMs ( err : unknown ) : number | null {
6793 if ( ! isRecord ( err ) ) {
6894 return null ;
@@ -129,6 +155,7 @@ export type MSTeamsSendErrorClassification = {
129155 kind : MSTeamsSendErrorKind ;
130156 statusCode ?: number ;
131157 retryAfterMs ?: number ;
158+ errorCode ?: string ;
132159} ;
133160
134161/**
@@ -142,16 +169,25 @@ export type MSTeamsSendErrorClassification = {
142169export function classifyMSTeamsSendError ( err : unknown ) : MSTeamsSendErrorClassification {
143170 const statusCode = extractStatusCode ( err ) ;
144171 const retryAfterMs = extractRetryAfterMs ( err ) ;
172+ const errorCode = extractErrorCode ( err ) ?? undefined ;
145173
146- if ( statusCode === 401 || statusCode === 403 ) {
147- return { kind : "auth" , statusCode } ;
174+ if ( statusCode === 401 ) {
175+ return { kind : "auth" , statusCode, errorCode } ;
176+ }
177+
178+ if ( statusCode === 403 ) {
179+ if ( errorCode === "ContentStreamNotAllowed" ) {
180+ return { kind : "permanent" , statusCode, errorCode } ;
181+ }
182+ return { kind : "auth" , statusCode, errorCode } ;
148183 }
149184
150185 if ( statusCode === 429 ) {
151186 return {
152187 kind : "throttled" ,
153188 statusCode,
154189 retryAfterMs : retryAfterMs ?? undefined ,
190+ errorCode,
155191 } ;
156192 }
157193
@@ -160,17 +196,19 @@ export function classifyMSTeamsSendError(err: unknown): MSTeamsSendErrorClassifi
160196 kind : "transient" ,
161197 statusCode,
162198 retryAfterMs : retryAfterMs ?? undefined ,
199+ errorCode,
163200 } ;
164201 }
165202
166203 if ( statusCode != null && statusCode >= 400 ) {
167- return { kind : "permanent" , statusCode } ;
204+ return { kind : "permanent" , statusCode, errorCode } ;
168205 }
169206
170207 return {
171208 kind : "unknown" ,
172209 statusCode : statusCode ?? undefined ,
173210 retryAfterMs : retryAfterMs ?? undefined ,
211+ errorCode,
174212 } ;
175213}
176214
@@ -195,6 +233,9 @@ export function formatMSTeamsSendErrorHint(
195233 if ( classification . kind === "auth" ) {
196234 return "check msteams appId/appPassword/tenantId (or env vars MSTEAMS_APP_ID/MSTEAMS_APP_PASSWORD/MSTEAMS_TENANT_ID)" ;
197235 }
236+ if ( classification . errorCode === "ContentStreamNotAllowed" ) {
237+ return "Teams expired the content stream; stop streaming earlier and fall back to normal message delivery" ;
238+ }
198239 if ( classification . kind === "throttled" ) {
199240 return "Teams throttled the bot; backing off may help" ;
200241 }
0 commit comments