handle-streaming-refusals
test-and-evaluate/strengthen-guardrails/handle-streaming-refusals
History
test-and-evaluate/strengthen-guardrails/handle-streaming-refusals First recorded · 353 lines, first recorded
## API response format ## Reset context after refusal ## Implementation guide ## Current refusal types ## Best practices ## Migration notes ## Next steps
The first capture of this source. The page was already there, and this is what it said.
---
title: Handle streaming refusals
url: https://platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals
description: Detect and handle refusal stop reasons in streaming responses, and retry refused requests on a fallback model.
---
Starting with Claude 4 models, streaming responses from Claude's API return **`stop_reason`: `"refusal"`** when streaming classifiers intervene to handle potential policy violations. This safety feature helps maintain content compliance during real-time streaming.
<Tip>
This page covers how refusals appear in streaming responses. For every `stop_reason` value and how to handle it, see [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons). To retry refused requests on another Claude model, see [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback).
</Tip>
## API response format
When streaming classifiers detect content that violates Anthropic's policies, the API returns this response:
```json
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "Hello.."
}
],
"stop_reason": "refusal",
"stop_details": {
"type": "refusal",
"category": "cyber",
"explanation": "This request was declined because it could enable cyber harm."
}
}
```
In the event stream, `stop_details` arrives on the `message_delta` event alongside `stop_reason`.
<Note>
A `refusal` response from streaming classifiers includes a `stop_details` object with a `category` and a human-readable `explanation` that you can surface to the user. See [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#refusal-response) for the full response shape and the available categories.
On a refusal the `stop_details` object is always present, but its `category` and `explanation` fields can be `null`, for example when the refusal maps to no named category. Branch on `stop_reason` or `stop_details.type` rather than assuming `category` and `explanation` are populated, and provide your own user-facing messaging when they are `null`.
</Note>
## Reset context after refusal
When you receive **`stop_reason`: `refusal`**, you must reset the conversation context before continuing. You can remove or rephrase the turn that triggered the refusal, or clear the conversation history entirely. Attempting to continue without resetting will result in continued refusals.
<Note>
Usage metrics are still provided in the response, even when the response is refused.
When a refusal arrives before Claude generates any output, you are not billed for the request on the Claude API, and the usage counts in that response are informational only. When Claude generates output before the refusal, you are billed for that request.
</Note>
<Tip>
Resetting context is not the only way to recover. You can also retry the refused request on a different Claude model, and the [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback) page shows how to set that up with server-side fallback, the SDK middleware, or a manual retry.
</Tip>
## Implementation guide
Here's how to detect and handle streaming refusals in your application:
<CodeGroup>
```bash cURL
# Stream request and check for refusal
response=$(curl -N https://api.anthropic.com/v1/messages \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 1024,
"stream": true
}')
# Check for refusal in the stream
if echo "$response" | grep -q '"stop_reason":"refusal"'; then
echo "Response refused - resetting conversation context"
# Reset your conversation state here
fi
```
```python Python
client = anthropic.Anthropic()
messages = []
def reset_conversation():
"""Reset conversation context after refusal"""
global messages
messages = []
print("Conversation reset due to refusal")
try:
with client.messages.stream(
max_tokens=1024,
messages=messages + [{"role": "user", "content": "Hello"}],
model="claude-opus-5",
) as stream:
for event in stream:
# Check for refusal in message delta
if event.type == "message_delta":
if event.delta.stop_reason == "refusal":
reset_conversation()
break
except Exception as e:
print(f"Error: {e}")
```
```typescript TypeScript
const client = new Anthropic();
let messages: Anthropic.MessageParam[] = [];
function resetConversation() {
// Reset conversation context after refusal
messages = [];
console.log("Conversation reset due to refusal");
}
try {
const stream = await client.messages.stream({
messages: [...messages, { role: "user", content: "Hello" }],
model: "claude-opus-5",
max_tokens: 1024
});
for await (const event of stream) {
// Check for refusal in message delta
if (event.type === "message_delta" && event.delta.stop_reason === "refusal") {
resetConversation();
break;
}
}
} catch (error) {
console.error("Error:", error);
}
```
```csharp C#
List<Message> messages = new();
AnthropicClient client = new();
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus5,
MaxTokens = 1024,
Messages = [new() { Role = Role.User, Content = "Hello" }]
};
try
{
await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
{
if (
streamEvent.TryPickDelta(out var deltaEvent)
&& deltaEvent.Delta.StopReason == StopReason.Refusal
)
{
ResetConversation();
break;
}
}
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.Message}");
}
void ResetConversation()
{
messages.Clear();
Console.WriteLine("Conversation reset due to refusal");
}
```
```go Go
var messages []anthropic.MessageParam
func resetConversation() {
messages = []anthropic.MessageParam{}
fmt.Println("Conversation reset due to refusal")
}
// ...
client := anthropic.NewClient()
stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Hello")),
},
})
streamLoop:
for stream.Next() {
event := stream.Current()
switch eventVariant := event.AsAny().(type) {
case anthropic.MessageDeltaEvent:
if eventVariant.Delta.StopReason == anthropic.StopReasonRefusal {
resetConversation()
break streamLoop
}
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}
```
```java Java
import com.anthropic.core.http.StreamResponse;
import com.anthropic.models.messages.RawMessageStreamEvent;
import com.anthropic.models.messages.StopReason;
// ...
List<MessageParam> messages = new ArrayList<>();
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5)
.maxTokens(1024L)
.addUserMessage("Hello")
.build();
try (StreamResponse<RawMessageStreamEvent> stream = client.messages().createStreaming(params)) {
stream.stream().forEach(event -> {
event.messageDelta().ifPresent(deltaEvent -> {
deltaEvent.delta().stopReason().ifPresent(stopReason -> {
if (stopReason.equals(StopReason.REFUSAL)) {
resetConversation();
}
});
});
});
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
void resetConversation() {
messages.clear();
IO.println("Conversation reset due to refusal");
}
```
```php PHP
$client = new Client();
$messages = [];
function resetConversation(&$messages) {
$messages = [];
echo "Conversation reset due to refusal\n";
}
try {
$stream = $client->messages->createStream(
maxTokens: 1024,
messages: [
['role' => 'user', 'content' => 'Hello']
],
model: 'claude-opus-5',
);
foreach ($stream as $event) {
if ($event->type === 'message_delta' && $event->delta->stopReason === 'refusal') {
resetConversation($messages);
break;
}
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
```
```ruby Ruby
client = Anthropic::Client.new
messages = []
def reset_conversation(messages)
messages.clear
puts "Conversation reset due to refusal"
end
begin
stream = client.messages.stream(
model: :"claude-opus-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello" }]
)
stream.each do |event|
if event.type == :message_delta && event.delta.stop_reason == :refusal
reset_conversation(messages)
break
end
end
rescue => e
Cut at 300 lines.