#9900 backport Accurate implementation of H2 Request.beginNanoTime()

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
Ludovic Orban 2023-11-23 14:50:44 +01:00 committed by Simone Bordet
parent f82844e2a2
commit 65c8b58843
7 changed files with 187 additions and 29 deletions

View File

@ -151,6 +151,13 @@ public class MetaData implements Iterable<HttpField>
version, fields, contentLength);
}
public Request(long beginNanoTime, String method, String scheme, HostPortHttpField authority, String uri, HttpVersion version, HttpFields fields, long contentLength)
{
this(beginNanoTime, method,
HttpURI.build().scheme(scheme).host(authority == null ? null : authority.getHost()).port(authority == null ? -1 : authority.getPort()).pathQuery(uri),
version, fields, contentLength);
}
public Request(String method, HttpURI uri, HttpVersion version, HttpFields fields, long contentLength, Supplier<HttpFields> trailers)
{
this(NanoTime.now(), method, uri, version, fields, contentLength, trailers);
@ -222,9 +229,19 @@ public class MetaData implements Iterable<HttpField>
this(scheme == null ? null : scheme.asString(), authority, path, fields, protocol);
}
public ConnectRequest(long beginNanoTime, HttpScheme scheme, HostPortHttpField authority, String path, HttpFields fields, String protocol)
{
this(beginNanoTime, scheme == null ? null : scheme.asString(), authority, path, fields, protocol);
}
public ConnectRequest(String scheme, HostPortHttpField authority, String path, HttpFields fields, String protocol)
{
super(HttpMethod.CONNECT.asString(),
this(NanoTime.now(), scheme, authority, path, fields, protocol);
}
public ConnectRequest(long beginNanoTime, String scheme, HostPortHttpField authority, String path, HttpFields fields, String protocol)
{
super(beginNanoTime, HttpMethod.CONNECT.asString(),
HttpURI.build().scheme(scheme).host(authority == null ? null : authority.getHost()).port(authority == null ? -1 : authority.getPort()).pathQuery(path),
HttpVersion.HTTP_2, fields, Long.MIN_VALUE, null);
_protocol = protocol;

View File

@ -30,6 +30,7 @@ import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.hpack.HpackDecoder;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.NanoTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -52,6 +53,8 @@ public class Parser
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private boolean continuation;
private State state = State.HEADER;
private long beginNanoTime;
private boolean nanoTimeStored;
@Deprecated
public Parser(ByteBufferPool byteBufferPool, int maxTableCapacity, int maxHeaderSize)
@ -74,7 +77,7 @@ public class Parser
{
this.byteBufferPool = byteBufferPool;
this.headerParser = new HeaderParser(rateControl == null ? RateControl.NO_RATE_CONTROL : rateControl);
this.hpackDecoder = new HpackDecoder(maxHeaderSize);
this.hpackDecoder = new HpackDecoder(maxHeaderSize, this::getBeginNanoTime);
this.bodyParsers = new BodyParser[FrameType.values().length];
}
@ -114,6 +117,25 @@ public class Parser
state = State.HEADER;
}
public long getBeginNanoTime()
{
return beginNanoTime;
}
private void clearBeginNanoTime()
{
nanoTimeStored = false;
}
private void storeBeginNanoTime()
{
if (!nanoTimeStored)
{
beginNanoTime = NanoTime.now();
nanoTimeStored = true;
}
}
/**
* <p>Parses the given {@code buffer} bytes and emit events to a {@link Listener}.</p>
* <p>When this method returns, the buffer may not be fully consumed, so invocations
@ -135,6 +157,7 @@ public class Parser
{
case HEADER:
{
storeBeginNanoTime();
if (!parseHeader(buffer))
return;
break;
@ -143,6 +166,8 @@ public class Parser
{
if (!parseBody(buffer))
return;
if (!continuation)
clearBeginNanoTime();
break;
}
default:

View File

@ -158,6 +158,102 @@ public class ContinuationParseTest
}
}
@Test
public void testBeginNanoTime() throws Exception
{
ByteBufferPool bufferPool = new MappedByteBufferPool(128);
HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(), new HpackEncoder());
final List<HeadersFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener.Adapter()
{
@Override
public void onHeaders(HeadersFrame frame)
{
frames.add(frame);
}
@Override
public void onConnectionFailure(int error, String reason)
{
frames.add(new HeadersFrame(null, null, false));
}
});
int streamId = 13;
HttpFields fields = HttpFields.build()
.put("Accept", "text/html")
.put("User-Agent", "Jetty");
MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP.asString(), new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields, -1);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(bufferPool);
generator.generateHeaders(lease, streamId, metaData, null, true);
List<ByteBuffer> byteBuffers = lease.getByteBuffers();
assertEquals(2, byteBuffers.size());
ByteBuffer headersBody = byteBuffers.remove(1);
int start = headersBody.position();
int length = headersBody.remaining();
int firstHalf = length / 2;
int lastHalf = length - firstHalf;
// Adjust the length of the HEADERS frame.
ByteBuffer headersHeader = byteBuffers.get(0);
headersHeader.put(0, (byte)((firstHalf >>> 16) & 0xFF));
headersHeader.put(1, (byte)((firstHalf >>> 8) & 0xFF));
headersHeader.put(2, (byte)(firstHalf & 0xFF));
// Remove the END_HEADERS flag from the HEADERS header.
headersHeader.put(4, (byte)(headersHeader.get(4) & ~Flags.END_HEADERS));
// New HEADERS body.
headersBody.position(start);
headersBody.limit(start + firstHalf);
byteBuffers.add(headersBody.slice());
// Split the rest of the HEADERS body into a CONTINUATION frame.
byte[] continuationHeader = new byte[9];
continuationHeader[0] = (byte)((lastHalf >>> 16) & 0xFF);
continuationHeader[1] = (byte)((lastHalf >>> 8) & 0xFF);
continuationHeader[2] = (byte)(lastHalf & 0xFF);
continuationHeader[3] = (byte)FrameType.CONTINUATION.getType();
continuationHeader[4] = Flags.END_HEADERS;
continuationHeader[5] = 0x00;
continuationHeader[6] = 0x00;
continuationHeader[7] = 0x00;
continuationHeader[8] = (byte)streamId;
byteBuffers.add(ByteBuffer.wrap(continuationHeader));
// CONTINUATION body.
headersBody.position(start + firstHalf);
headersBody.limit(start + length);
byteBuffers.add(headersBody.slice());
assertEquals(4, byteBuffers.size());
parser.parse(byteBuffers.get(0));
long beginNanoTime = parser.getBeginNanoTime();
parser.parse(byteBuffers.get(1));
parser.parse(byteBuffers.get(2));
parser.parse(byteBuffers.get(3));
assertEquals(1, frames.size());
HeadersFrame frame = frames.get(0);
assertEquals(streamId, frame.getStreamId());
assertTrue(frame.isEndStream());
MetaData.Request request = (MetaData.Request)frame.getMetaData();
assertEquals(metaData.getMethod(), request.getMethod());
assertEquals(metaData.getURIString(), request.getURIString());
for (int i = 0; i < fields.size(); ++i)
{
HttpField field = fields.getField(i);
assertTrue(request.getFields().contains(field));
}
PriorityFrame priority = frame.getPriority();
assertNull(priority);
assertEquals(beginNanoTime, request.getBeginNanoTime());
}
@Test
public void testLargeHeadersBlock() throws Exception
{

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import java.util.function.LongSupplier;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
@ -42,14 +43,18 @@ public class HpackDecoder
private final MetaDataBuilder _builder;
private final HuffmanDecoder _huffmanDecoder;
private final NBitIntegerDecoder _integerDecoder;
private final LongSupplier _beginNanoTimeSupplier;
private int _maxTableCapacity;
/**
* @param maxHeaderSize The maximum allowed size of a decoded headers block,
* expressed as total of all name and value bytes, plus 32 bytes per field
* @param beginNanoTimeSupplier The supplier of a nano timestamp taken at
* the time the first byte was read
*/
public HpackDecoder(int maxHeaderSize)
public HpackDecoder(int maxHeaderSize, LongSupplier beginNanoTimeSupplier)
{
_beginNanoTimeSupplier = beginNanoTimeSupplier;
_context = new HpackContext(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
_builder = new MetaDataBuilder(maxHeaderSize);
_huffmanDecoder = new HuffmanDecoder();
@ -305,6 +310,7 @@ public class HpackDecoder
}
}
_builder.setBeginNanoTime(_beginNanoTimeSupplier.getAsLong());
return _builder.build();
}

View File

@ -22,6 +22,7 @@ import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.hpack.HpackException.SessionException;
import org.eclipse.jetty.util.NanoTime;
public class MetaDataBuilder
{
@ -38,6 +39,7 @@ public class MetaDataBuilder
private HpackException.StreamException _streamException;
private boolean _request;
private boolean _response;
private long _beginNanoTime = Long.MIN_VALUE;
/**
* @param maxHeadersSize The maximum size of the headers, expressed as total name and value characters.
@ -60,6 +62,13 @@ public class MetaDataBuilder
_maxSize = maxSize;
}
public void setBeginNanoTime(long beginNanoTime)
{
if (beginNanoTime == Long.MIN_VALUE)
beginNanoTime++;
_beginNanoTime = beginNanoTime;
}
/**
* Get the size.
*
@ -248,10 +257,13 @@ public class MetaDataBuilder
if (_path == null)
throw new HpackException.StreamException("No Path");
}
long nanoTime = _beginNanoTime == Long.MIN_VALUE ? NanoTime.now() : _beginNanoTime;
_beginNanoTime = Long.MIN_VALUE;
if (isConnect)
return new MetaData.ConnectRequest(_scheme, _authority, _path, fields, _protocol);
return new MetaData.ConnectRequest(nanoTime, _scheme, _authority, _path, fields, _protocol);
else
return new MetaData.Request(
nanoTime,
_method,
_scheme.asString(),
_authority,

View File

@ -23,6 +23,7 @@ import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.hpack.HpackException.CompressionException;
import org.eclipse.jetty.http2.hpack.HpackException.SessionException;
import org.eclipse.jetty.http2.hpack.HpackException.StreamException;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.StringUtil;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
@ -57,7 +58,7 @@ public class HpackDecoderTest
@Test
public void testDecodeD3() throws Exception
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
// First request
String encoded = "828684410f7777772e6578616d706c652e636f6d";
@ -105,7 +106,7 @@ public class HpackDecoderTest
@Test
public void testDecodeD4() throws Exception
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
// First request
String encoded = "828684418cf1e3c2e5f23a6ba0ab90f4ff";
@ -140,7 +141,7 @@ public class HpackDecoderTest
{
String value = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "8682418cF1E3C2E5F23a6bA0Ab90F4Ff841f0822426173696320515778685a475270626a70766347567549484e6c633246745a513d3d";
byte[] bytes = StringUtil.fromHexString(encoded);
byte[] array = new byte[bytes.length + 1];
@ -162,7 +163,7 @@ public class HpackDecoderTest
@Test
public void testDecodeHuffmanWithArrayOffset() throws Exception
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "8286418cf1e3c2e5f23a6ba0ab90f4ff84";
byte[] bytes = StringUtil.fromHexString(encoded);
@ -186,7 +187,7 @@ public class HpackDecoderTest
String encoded = "886196C361Be940b6a65B6850400B8A00571972e080a62D1Bf5f87497cA589D34d1f9a0f0d0234327690Aa69D29aFcA954D3A5358980Ae112e0f7c880aE152A9A74a6bF3";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
MetaData.Response response = (MetaData.Response)decoder.decode(buffer);
assertThat(response.getStatus(), is(200));
@ -204,7 +205,7 @@ public class HpackDecoderTest
{
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
MetaData metaData = decoder.decode(buffer);
assertThat(metaData.getFields().get(HttpHeader.HOST), is("localhost0"));
assertThat(metaData.getFields().get(HttpHeader.COOKIE), is("abcdefghij"));
@ -226,7 +227,7 @@ public class HpackDecoderTest
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f20";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
try
{
decoder.decode(buffer);
@ -244,7 +245,7 @@ public class HpackDecoderTest
String encoded = "3f610f17FfEc02Df3990A190A0D4Ee5b3d2940Ec98Aa4a62D127D29e273a0aA20dEcAa190a503b262d8a2671D4A2672a927aA874988a2471D05510750c951139EdA2452a3a548cAa1aA90bE4B228342864A9E0D450A5474a92992a1aA513395448E3A0Aa17B96cFe3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f14E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F353F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F54f";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
decoder.setMaxTableCapacity(128);
MetaData metaData = decoder.decode(buffer);
@ -258,7 +259,7 @@ public class HpackDecoderTest
String encoded = "BE";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
decoder.setMaxTableCapacity(128);
try
@ -444,7 +445,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedStandard() throws Exception
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "82868441" + "83" + "49509F";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -462,7 +463,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedExtraPadding()
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "82868441" + "84" + "49509FFF";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -474,7 +475,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedZeroPadding()
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "82868441" + "83" + "495090";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -487,7 +488,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedWithEOS()
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "82868441" + "87" + "497FFFFFFF427F";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -499,7 +500,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedOneIncompleteOctet()
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "82868441" + "81" + "FE";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -511,7 +512,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedTwoIncompleteOctet()
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "82868441" + "82" + "FFFE";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -523,7 +524,7 @@ public class HpackDecoderTest
@Test
public void testZeroLengthName()
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "00000130";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -534,7 +535,7 @@ public class HpackDecoderTest
@Test
public void testZeroLengthValue() throws Exception
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "00016800";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -546,7 +547,7 @@ public class HpackDecoderTest
@Test
public void testUpperCaseName()
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "0001480130";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -557,7 +558,7 @@ public class HpackDecoderTest
@Test
public void testWhiteSpaceName()
{
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
String encoded = "0001200130";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));

View File

@ -24,6 +24,7 @@ import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MetaData.Response;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.NanoTime;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
@ -43,7 +44,7 @@ public class HpackTest
public void encodeDecodeResponseTest() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(8192);
HpackDecoder decoder = new HpackDecoder(8192, NanoTime::now);
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
HttpFields.Mutable fields0 = HttpFields.build()
@ -98,7 +99,7 @@ public class HpackTest
public void encodeDecodeTooLargeTest() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(164);
HpackDecoder decoder = new HpackDecoder(164, NanoTime::now);
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
HttpFields fields0 = HttpFields.build()
@ -158,7 +159,7 @@ public class HpackTest
@Test
public void evictReferencedFieldTest() throws Exception
{
HpackDecoder decoder = new HpackDecoder(1024);
HpackDecoder decoder = new HpackDecoder(1024, NanoTime::now);
decoder.setMaxTableCapacity(200);
HpackEncoder encoder = new HpackEncoder();
encoder.setMaxTableCapacity(decoder.getMaxTableCapacity());
@ -205,7 +206,7 @@ public class HpackTest
public void testHopHeadersAreRemoved() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(16384);
HpackDecoder decoder = new HpackDecoder(16384, NanoTime::now);
HttpFields input = HttpFields.build()
.add(HttpHeader.ACCEPT, "*")
@ -232,7 +233,7 @@ public class HpackTest
public void testTETrailers() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(16384);
HpackDecoder decoder = new HpackDecoder(16384, NanoTime::now);
String teValue = "trailers";
String trailerValue = "Custom";
@ -257,7 +258,7 @@ public class HpackTest
public void testColonHeaders() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(16384);
HpackDecoder decoder = new HpackDecoder(16384, NanoTime::now);
HttpFields input = HttpFields.build()
.add(":status", "200")