Use Azure Service Bus output binding to send queue or topic messages.
For information on setup and configuration details, see the overview.
Important
This article uses tabs to support multiple versions of the Node.js programming model. The v4 model is generally available and is designed to have a more flexible and intuitive experience for JavaScript and TypeScript developers. For more details about how the v4 model works, refer to the Azure Functions Node.js developer guide. To learn more about the differences between v3 and v4, refer to the migration guide.
Azure Functions supports two programming models for Python. The way that you define your bindings depends on your chosen programming model.
The Python v2 programming model lets you define bindings using decorators directly in your Python function code. For more information, see the Python developer guide.
The Python v1 programming model requires you to define bindings in a separate function.json file in the function folder. For more information, see the Python developer guide.
This article supports both programming models.
ExampleA C# function can be created by using one of the following C# modes:
Microsoft.Azure.Functions.Worker.Extensions.*
namespaces.Microsoft.Azure.WebJobs.Extensions.*
namespaces.This code defines and initializes the ILogger
:
private readonly ILogger<ServiceBusReceivedMessageFunctions> _logger;
public ServiceBusReceivedMessageFunctions(ILogger<ServiceBusReceivedMessageFunctions> logger)
{
_logger = logger;
}
This example shows a C# function that receives a message and writes it to a second queue:
[Function(nameof(ServiceBusReceivedMessageFunction))]
[ServiceBusOutput("outputQueue", Connection = "ServiceBusConnection")]
public string ServiceBusReceivedMessageFunction(
[ServiceBusTrigger("queue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
{
_logger.LogInformation("Message ID: {id}", message.MessageId);
_logger.LogInformation("Message Body: {body}", message.Body);
_logger.LogInformation("Message Content-Type: {contentType}", message.ContentType);
var outputMessage = $"Output message created at {DateTime.Now}";
return outputMessage;
}
This example uses an HTTP trigger with an OutputType
object to both send an HTTP response and write the output message.
[Function("HttpSendMsg")]
public async Task<OutputType> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req, FunctionContext context)
{
_logger.LogInformation($"C# HTTP trigger function processed a request for {context.InvocationId}.");
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteStringAsync("HTTP response: Message sent");
return new OutputType()
{
OutputEvent = "MyMessage",
HttpResponse = response
};
}
This code defines the multiple output type OutputType
, which includes the Service Bus output binding definition on OutputEvent
:
public class OutputType
{
[ServiceBusOutput("TopicOrQueueName", Connection = "ServiceBusConnection")]
public string OutputEvent { get; set; }
public HttpResponseData HttpResponse { get; set; }
}
The following example shows a C# function that sends a Service Bus queue message:
[FunctionName("ServiceBusOutput")]
[return: ServiceBus("myqueue", Connection = "ServiceBusConnection")]
public static string ServiceBusOutput([HttpTrigger] dynamic input, ILogger log)
{
log.LogInformation($"C# function processed: {input.Text}");
return input.Text;
}
Instead of using the return statement to send the message, this HTTP trigger function returns an HTTP response that is different from the output message.
[FunctionName("HttpTrigger1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
[ServiceBus("TopicOrQueueName", Connection = "ServiceBusConnection")] IAsyncCollector<string> message, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
await message.AddAsync("MyMessage");
await message.AddAsync("MyMessage2");
string responseMessage = "This HTTP triggered sent a message to Service Bus.";
return new OkObjectResult(responseMessage);
}
The following example shows a Java function that sends a message to a Service Bus queue myqueue
when triggered by an HTTP request.
@FunctionName("httpToServiceBusQueue")
@ServiceBusQueueOutput(name = "message", queueName = "myqueue", connection = "AzureServiceBusConnection")
public String pushToQueue(
@HttpTrigger(name = "request", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS)
final String message,
@HttpOutput(name = "response") final OutputBinding<T> result ) {
result.setValue(message + " has been sent.");
return message;
}
In the Java functions runtime library, use the @QueueOutput
annotation on function parameters whose value would be written to a Service Bus queue. The parameter type should be OutputBinding<T>
, where T
is any native Java type of a plan old Java object (POJO).
Java functions can also write to a Service Bus topic. The following example uses the @ServiceBusTopicOutput
annotation to describe the configuration for the output binding.
@FunctionName("sbtopicsend")
public HttpResponseMessage run(
@HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
@ServiceBusTopicOutput(name = "message", topicName = "mytopicname", subscriptionName = "mysubscription", connection = "ServiceBusConnection") OutputBinding<String> message,
final ExecutionContext context) {
String name = request.getBody().orElse("Azure Functions");
message.setValue(name);
return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
}
The following example shows a timer triggered TypeScript function that sends a queue message every 5 minutes.
import { app, InvocationContext, output, Timer } from '@azure/functions';
export async function timerTrigger1(myTimer: Timer, context: InvocationContext): Promise<string> {
const timeStamp = new Date().toISOString();
return `Message created at: ${timeStamp}`;
}
app.timer('timerTrigger1', {
schedule: '0 */5 * * * *',
return: output.serviceBusQueue({
queueName: 'testqueue',
connection: 'MyServiceBusConnection',
}),
handler: timerTrigger1,
});
To output multiple messages, return an array instead of a single object. For example:
const timeStamp = new Date().toISOString();
const message = `Message created at: ${timeStamp}`;
return [`1: ${message}`, `2: ${message}`];
TypeScript samples aren't documented for model v3.
The following example shows a timer triggered JavaScript function that sends a queue message every 5 minutes.
const { app, output } = require('@azure/functions');
const serviceBusOutput = output.serviceBusQueue({
queueName: 'testqueue',
connection: 'MyServiceBusConnection',
});
app.timer('timerTrigger1', {
schedule: '0 */5 * * * *',
return: serviceBusOutput,
handler: (myTimer, context) => {
const timeStamp = new Date().toISOString();
return `Message created at: ${timeStamp}`;
},
});
To output multiple messages, return an array instead of a single object. For example:
const timeStamp = new Date().toISOString();
const message = `Message created at: ${timeStamp}`;
return [`1: ${message}`, `2: ${message}`];
The following example shows a Service Bus output binding in a function.json file and a JavaScript function that uses the binding. The function uses a timer trigger to send a queue message every 15 seconds.
Here's the binding data in the function.json file:
{
"bindings": [
{
"schedule": "0/15 * * * * *",
"name": "myTimer",
"runsOnStartup": true,
"type": "timerTrigger",
"direction": "in"
},
{
"name": "outputSbQueue",
"type": "serviceBus",
"queueName": "testqueue",
"connection": "MyServiceBusConnection",
"direction": "out"
}
],
"disabled": false
}
Here's JavaScript script code that creates a single message:
module.exports = async function (context, myTimer) {
var message = 'Service Bus queue message created at ' + timeStamp;
context.log(message);
context.bindings.outputSbQueue = message;
};
Here's JavaScript script code that creates multiple messages:
module.exports = async function (context, myTimer) {
var message = 'Service Bus queue message created at ' + timeStamp;
context.log(message);
context.bindings.outputSbQueue = [];
context.bindings.outputSbQueue.push("1 " + message);
context.bindings.outputSbQueue.push("2 " + message);
};
The following example shows a Service Bus output binding in a function.json file and a PowerShell function that uses the binding.
Here's the binding data in the function.json file:
{
"bindings": [
{
"type": "serviceBus",
"direction": "out",
"connection": "AzureServiceBusConnectionString",
"name": "outputSbMsg",
"queueName": "outqueue",
"topicName": "outtopic"
}
]
}
Here's the PowerShell that creates a message as the function's output.
param($QueueItem, $TriggerMetadata)
Push-OutputBinding -Name outputSbMsg -Value @{
name = $QueueItem.name
employeeId = $QueueItem.employeeId
address = $QueueItem.address
}
The following example demonstrates how to write out to a Service Bus topics and Service Bus queues in Python. The example depends on whether you use the v1 or v2 Python programming model.
This example shows how to write out to a Service Bus topic.
import logging
import azure.functions as func
app = func.FunctionApp()
@app.route(route="put_message")
@app.service_bus_topic_output(arg_name="message",
connection="AzureServiceBusConnectionString",
topic_name="outTopic")
def main(req: func.HttpRequest, message: func.Out[str]) -> func.HttpResponse:
input_msg = req.params.get('message')
message.set(input_msg)
return 'OK'
This example shows how to write out to a Service Bus queue.
import azure.functions as func
app = func.FunctionApp()
@app.route(route="put_message")
@app.service_bus_queue_output(
arg_name="msg",
connection="AzureServiceBusConnectionString",
queue_name="outqueue")
def put_message(req: func.HttpRequest, msg: func.Out[str]):
msg.set(req.get_body().decode('utf-8'))
return 'OK'
A Service Bus binding definition is defined in function.json where type is set to serviceBus
. This example shows how to write out to a Service Bus topic.
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
},
{
"type": "serviceBus",
"direction": "out",
"connection": "AzureServiceBusConnectionString",
"name": "msg",
"topicName": "outTopic"
}
]
}
In _init_.py, you can write out a message to the queue by passing a value to the set
method.
import azure.functions as func
def main(req: azf.HttpRequest, msg: azf.Out[str]):
msg.set(req.get_body().decode('utf-8'))
return 'OK'
This example shows how to write out to a Service Bus queue.
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
},
{
"type": "serviceBus",
"direction": "out",
"connection": "AzureServiceBusConnectionString",
"name": "msg",
"queueName": "outqueue"
}
]
}
import azure.functions as func
def main(req: func.HttpRequest, msg: func.Out[str]) -> func.HttpResponse:
input_msg = req.params.get('message')
msg.set(input_msg)
return 'OK'
Attributes
Both in-process and isolated worker process C# libraries use attributes to define the output binding. C# script instead uses a function.json configuration file as described in the C# scripting guide.
In C# class libraries, use the ServiceBusOutputAttribute to define the queue or topic written to by the output.
The following table explains the properties you can set using the attribute:
Property Description EntityType Sets the entity type as eitherQueue
for sending messages to a queue or Topic
when sending messages to a topic. QueueOrTopicName Name of the topic or queue to send messages to. Use EntityType
to set the destination type. Connection The name of an app setting or setting collection that specifies how to connect to Service Bus. See Connections.
In C# class libraries, use the ServiceBusAttribute.
The following table explains the properties you can set using the attribute:
Property Description QueueName Name of the queue. Set only if sending queue messages, not for a topic. TopicName Name of the topic. Set only if sending topic messages, not for a queue. Connection The name of an app setting or setting collection that specifies how to connect to Service Bus. See Connections. Access Access rights for the connection string. Available values aremanage
and listen
. The default is manage
, which indicates that the connection
has the Manage permission. If you use a connection string that doesn't have the Manage permission, set accessRights
to "listen". Otherwise, the Functions runtime might fail trying to do operations that require manage rights. In Azure Functions version 2.x and higher, this property isn't available because the latest version of the Service Bus SDK doesn't support manage operations.
Here's an example that shows the attribute applied to the return value of the function:
[FunctionName("ServiceBusOutput")]
[return: ServiceBus("myqueue")]
public static string Run([HttpTrigger] dynamic input, ILogger log)
{
...
}
You can set the Connection
property to specify the name of an app setting that contains the Service Bus connection string to use, as shown in the following example:
[FunctionName("ServiceBusOutput")]
[return: ServiceBus("myqueue", Connection = "ServiceBusConnection")]
public static string Run([HttpTrigger] dynamic input, ILogger log)
{
...
}
For a complete example, see Example.
You can use the ServiceBusAccount
attribute to specify the Service Bus account to use at class, method, or parameter level. For more information, see Attributes in the trigger reference.
Applies only to the Python v2 programming model.
For Python v2 functions defined using a decorator, the following properties on the service_bus_topic_output
:
arg_name
The name of the variable that represents the queue or topic message in function code. queue_name
Name of the queue. Set only if sending queue messages, not for a topic. topic_name
Name of the topic. Set only if sending topic messages, not for a queue. connection
The name of an app setting or setting collection that specifies how to connect to Service Bus. See Connections.
For Python functions defined by using function.json, see the Configuration section.
AnnotationsThe ServiceBusQueueOutput
and ServiceBusTopicOutput
annotations are available to write a message as a function output. The parameter decorated with these annotations must be declared as an OutputBinding<T>
where T
is the type corresponding to the message's type.
When you're developing locally, add your application settings in the local.settings.json file in the Values
collection.
Applies only to the Python v1 programming model.
The following table explains the properties that you can set on the options
object passed to the output.serviceBusQueue()
method.
The following table explains the properties that you can set on the options
object passed to the output.serviceBusTopic()
method.
The following table explains the binding configuration properties that you set in the function.json file.
Property Description type Must be set toserviceBus
. This property is set automatically when you create the trigger in the Azure portal. direction Must be set to out
. This property is set automatically when you create the trigger in the Azure portal. name The name of the variable that represents the queue or topic message in function code. Set to "$return" to reference the function return value. queueName Name of the queue. Set only if sending queue messages, not for a topic. topicName Name of the topic. Set only if sending topic messages, not for a queue. connection The name of an app setting or setting collection that specifies how to connect to Service Bus. See Connections.
When you're developing locally, add your application settings in the local.settings.json file in the Values
collection.
The following table explains the binding configuration properties that you set in the function.json file and the ServiceBus
attribute.
serviceBus
. This property is set automatically when you create the trigger in the Azure portal. direction Must be set to out
. This property is set automatically when you create the trigger in the Azure portal. name The name of the variable that represents the queue or topic message in function code. Set to "$return" to reference the function return value. queueName Name of the queue. Set only if sending queue messages, not for a topic. topicName Name of the topic. Set only if sending topic messages, not for a queue. connection The name of an app setting or setting collection that specifies how to connect to Service Bus. See Connections. accessRights (v1 only) Access rights for the connection string. Available values are manage
and listen
. The default is manage
, which indicates that the connection
has the Manage permission. If you use a connection string that doesn't have the Manage permission, set accessRights
to "listen". Otherwise, the Functions runtime might fail trying to do operations that require manage rights. In Azure Functions version 2.x and higher, this property isn't available because the latest version of the Service Bus SDK doesn't support manage operations.
When you're developing locally, add your application settings in the local.settings.json file in the Values
collection.
See the Example section for complete examples.
UsageAll C# modalities and extension versions support the following output parameter types:
Type Description System.String Use when the message to write is simple text. When the parameter value is null when the function exits, Functions doesn't create a message. byte[] Use for writing binary data messages. When the parameter value is null when the function exits, Functions doesn't create a message. Object When a message contains JSON, Functions serializes the object into a JSON message payload. When the parameter value is null when the function exits, Functions creates a message with a null object.Messaging-specific parameter types contain extra message metadata and aren't compatible with JSON serialization. As a result, it isn't possible to use ServiceBusMessage
with the output binding in the isolated model. The specific types supported by the output binding depend on the Functions runtime version, the extension package version, and the C# modality used.
Use the ServiceBusMessage type when sending messages with metadata. Parameters are defined as return
type attributes. Use an ICollector<T>
or IAsyncCollector<T>
to write multiple messages. A message is created when you call the Add
method.
When the parameter value is null when the function exits, Functions doesn't create a message.
You can also use the ServiceBusAccountAttribute to specify the Service Bus account to use. The constructor takes the name of an app setting that contains a Service Bus connection string. The attribute can be applied at the parameter, method, or class level. The following example shows class level and method level:
[ServiceBusAccount("ClassLevelServiceBusAppSetting")]
public static class AzureFunctions
{
[ServiceBusAccount("MethodLevelServiceBusAppSetting")]
[FunctionName("ServiceBusQueueTriggerCSharp")]
public static void Run(
[ServiceBusTrigger("myqueue", AccessRights.Manage)]
string myQueueItem, ILogger log)
{
...
}
The Service Bus account to use is determined in the following order:
ServiceBusTrigger
attribute's Connection
property.ServiceBusAccount
attribute applied to the same parameter as the ServiceBusTrigger
attribute.ServiceBusAccount
attribute applied to the function.ServiceBusAccount
attribute applied to the class.AzureWebJobsServiceBus
app setting.Use the Message type when sending messages with metadata. Parameters are defined as return
type attributes. Use an ICollector<T>
or IAsyncCollector<T>
to write multiple messages. A message is created when you call the Add
method.
On 30 September 2026, we'll retire the Azure Service Bus SDK libraries WindowsAzure.ServiceBus, Microsoft.Azure.ServiceBus, and com.microsoft.azure.servicebus, which don't conform to Azure SDK guidelines. We'll also end support of the SBMP protocol, so you'll no longer be able to use this protocol after 30 September 2026. Migrate to the latest Azure SDK libraries, which offer critical security updates and improved capabilities, before that date.
Although the older libraries can still be used beyond 30 September 2026, they'll no longer receive official support and updates from Microsoft. For more information, see the support retirement announcement.
When the parameter value is null when the function exits, Functions doesn't create a message.
You can also use the ServiceBusAccountAttribute to specify the Service Bus account to use. The constructor takes the name of an app setting that contains a Service Bus connection string. The attribute can be applied at the parameter, method, or class level. The following example shows class level and method level:
[ServiceBusAccount("ClassLevelServiceBusAppSetting")]
public static class AzureFunctions
{
[ServiceBusAccount("MethodLevelServiceBusAppSetting")]
[FunctionName("ServiceBusQueueTriggerCSharp")]
public static void Run(
[ServiceBusTrigger("myqueue", AccessRights.Manage)]
string myQueueItem, ILogger log)
{
...
}
The Service Bus account to use is determined in the following order:
ServiceBusTrigger
attribute's Connection
property.ServiceBusAccount
attribute applied to the same parameter as the ServiceBusTrigger
attribute.ServiceBusAccount
attribute applied to the function.ServiceBusAccount
attribute applied to the class.AzureWebJobsServiceBus
app setting.Use the BrokeredMessage type when sending messages with metadata. Parameters are defined as return
type attributes. When the parameter value is null when the function exits, Functions doesn't create a message.
You can also use the ServiceBusAccountAttribute to specify the Service Bus account to use. The constructor takes the name of an app setting that contains a Service Bus connection string. The attribute can be applied at the parameter, method, or class level. The following example shows class level and method level:
[ServiceBusAccount("ClassLevelServiceBusAppSetting")]
public static class AzureFunctions
{
[ServiceBusAccount("MethodLevelServiceBusAppSetting")]
[FunctionName("ServiceBusQueueTriggerCSharp")]
public static void Run(
[ServiceBusTrigger("myqueue", AccessRights.Manage)]
string myQueueItem, ILogger log)
{
...
}
The Service Bus account to use is determined in the following order:
ServiceBusTrigger
attribute's Connection
property.ServiceBusAccount
attribute applied to the same parameter as the ServiceBusTrigger
attribute.ServiceBusAccount
attribute applied to the function.ServiceBusAccount
attribute applied to the class.AzureWebJobsServiceBus
app setting.When you want the function to write a single message, the Service Bus output binding can bind to the following types:
Type Descriptionstring
The message as a string. Use when the message is simple text. byte[]
The bytes of the message. JSON serializable types An object representing the message. Functions attempts to serialize a plain-old CLR object (POCO) type into JSON data.
When you want the function to write multiple messages, the Service Bus output binding can bind to the following types:
Type DescriptionT[]
where T
is one of the single message types An array containing multiple message. Each entry represents one message.
For other output scenarios, create and use a ServiceBusClient with other types from Azure.Messaging.ServiceBus directly. See Register Azure clients for an example of using dependency injection to create a client type from the Azure SDK.
Earlier versions of this extension in the isolated worker process only support binding to messaging-specific types. More options are available to Extension 5.x and higher
Functions version 1.x doesn't support isolated worker process. To use the isolated worker model, upgrade your application to Functions 4.x.
In Azure Functions 1.x, the runtime creates the queue if it doesn't exist and you have set accessRights
to manage
. In Azure Functions version 2.x and higher, the queue or topic must already exist; if you specify a queue or topic that doesn't exist, the function fails.
Use the Azure Service Bus SDK rather than the built-in output binding.
Access the output message by returning the value directly or using context.extraOutputs.set()
.
Access the output message by using context.bindings.<name>
where <name>
is the value specified in the name
property of function.json.
Output to the Service Bus is available via the Push-OutputBinding
cmdlet where you pass arguments that match the name designated by binding's name parameter in the function.json file.
The output function parameter must be defined as func.Out[str]
or func.Out[bytes]
. Refer to the output example for details. Alternatively, you can use the Azure Service Bus SDK rather than the built-in output binding.
For a complete example, see the examples section.
ConnectionsThe connection
property is a reference to environment configuration which specifies how the app should connect to Service Bus. It may specify:
If the configured value is both an exact match for a single setting and a prefix match for other settings, the exact match is used.
Connection stringTo obtain a connection string, follow the steps shown at Get the management credentials. The connection string must be for a Service Bus namespace, not limited to a specific queue or topic.
This connection string should be stored in an application setting with a name matching the value specified by the connection
property of the binding configuration.
If the app setting name begins with "AzureWebJobs", you can specify only the remainder of the name. For example, if you set connection
to "MyServiceBus", the Functions runtime looks for an app setting that is named "AzureWebJobsMyServiceBus". If you leave connection
empty, the Functions runtime uses the default Service Bus connection string in the app setting that is named "AzureWebJobsServiceBus".
If you are using version 5.x or higher of the extension, instead of using a connection string with a secret, you can have the app use an Microsoft Entra identity. To do this, you would define settings under a common prefix which maps to the connection
property in the trigger and binding configuration.
In this mode, the extension requires the following properties:
Property Environment variable template Description Example value Fully Qualified Namespace<CONNECTION_NAME_PREFIX>__fullyQualifiedNamespace
The fully qualified Service Bus namespace. <service_bus_namespace>.servicebus.windows.net
Additional properties may be set to customize the connection. See Common properties for identity-based connections.
Note
When using Azure App Configuration or Key Vault to provide settings for Managed Identity connections, setting names should use a valid key separator such as :
or /
in place of the __
to ensure names are resolved correctly.
For example, <CONNECTION_NAME_PREFIX>:fullyQualifiedNamespace
.
When hosted in the Azure Functions service, identity-based connections use a managed identity. The system-assigned identity is used by default, although a user-assigned identity can be specified with the credential
and clientID
properties. Note that configuring a user-assigned identity with a resource ID is not supported. When run in other contexts, such as local development, your developer identity is used instead, although this can be customized. See Local development with identity-based connections.
Whatever identity is being used must have permissions to perform the intended actions. For most Azure services, this means you need to assign a role in Azure RBAC, using either built-in or custom roles which provide those permissions.
Important
Some permissions might be exposed by the target service that are not necessary for all contexts. Where possible, adhere to the principle of least privilege, granting the identity only required privileges. For example, if the app only needs to be able to read from a data source, use a role that only has permission to read. It would be inappropriate to assign a role that also allows writing to that service, as this would be excessive permission for a read operation. Similarly, you would want to ensure the role assignment is scoped only over the resources that need to be read.
You'll need to create a role assignment that provides access to your topics and queues at runtime. Management roles like Owner aren't sufficient. The following table shows built-in roles that are recommended when using the Service Bus extension in normal operation. Your application may require additional permissions based on the code you write.
1 For triggering from Service Bus topics, the role assignment needs to have effective scope over the Service Bus subscription resource. If only the topic is included, an error will occur. Some clients, such as the Azure portal, don't expose the Service Bus subscription resource as a scope for role assignment. In such cases, the Azure CLI may be used instead. To learn more, see Azure built-in roles for Azure Service Bus.
Exceptions and return codes Next stepsRetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4